diff --git a/src/common/INet/LOAuth2.cpp b/src/common/INet/LOAuth2.cpp --- a/src/common/INet/LOAuth2.cpp +++ b/src/common/INet/LOAuth2.cpp @@ -1,439 +1,497 @@ #include "Lgi.h" #include "resdefs.h" #include "GTextLog.h" #include "OpenSSLSocket.h" #include "Base64.h" #include "INetTools.h" #include "LOAuth2.h" #include "LJSon.h" ////////////////////////////////////////////////////////////////// #define LOCALHOST_PORT 54900 #define OPT_AccessToken "AccessToken" #define OPT_RefreshToken "RefreshToken" static GString GetHeaders(GSocketI *s) { char Buf[256]; ssize_t Rd; GString p; while ((Rd = s->Read(Buf, sizeof(Buf))) > 0) { p += GString(Buf, Rd); if (p.Find("\r\n\r\n") >= 0) return p; } s->Close(); return NULL; } ssize_t ChunkSize(ssize_t &Pos, GString &Buf, GString &Body) { static GString Eol("\r\n"); auto End = Buf.Find(Eol, Pos); if (End > Pos) { auto Sz = Buf(Pos, End).Int(16); if (Sz >= 0) { End += Eol.Length(); auto Bytes = End + Sz + Eol.Length(); if (Buf.Length() >= Bytes) { Body += Buf(End, End + Sz); Pos = End + Sz + Eol.Length(); return Sz; } } } return -1; } static bool GetHttp(GSocketI *s, GString &Hdrs, GString &Body, bool IsResponse) { GString Resp = GetHeaders(s); char Buf[512]; ssize_t Rd; auto BodyPos = Resp.Find("\r\n\r\n"); GAutoString Len(InetGetHeaderField(Resp, "Content-Length", BodyPos)); if (Len) { int Bytes = atoi(Len); size_t Total = BodyPos + 4 + Bytes; while (Resp.Length() < Total) { Rd = s->Read(Buf, sizeof(Buf)); if (Rd > 0) { Resp += GString(Buf, Rd); } } } else if (s->IsOpen() && IsResponse) { GAutoString Te(InetGetHeaderField(Resp, "Transfer-Encoding", BodyPos)); bool Chunked = Te && !_stricmp(Te, "chunked"); if (Chunked) { ssize_t Pos = 0; Hdrs = Resp(0, BodyPos); GString Raw = Resp(BodyPos + 4, -1); Body.Empty(); while (s->IsOpen()) { auto Sz = ChunkSize(Pos, Raw, Body); if (Sz == 0) break; if (Sz < 0) { Rd = s->Read(Buf, sizeof(Buf)); if (Rd > 0) Raw += GString(Buf, Rd); else break; } } return true; } else { while ((Rd = s->Read(Buf, sizeof(Buf))) > 0) Resp += GString(Buf, Rd); } } Hdrs = Resp(0, BodyPos); Body = Resp(BodyPos + 4, -1); return true; } static GString UrlFromHeaders(GString Hdrs) { auto Lines = Hdrs.Split("\r\n", 1); auto p = Lines[0].SplitDelimit(); if (p.Length() < 3) { return NULL; } return p[1]; } static bool Write(GSocketI *s, GString b) { for (size_t i = 0; i < b.Length(); ) { auto Wr = s->Write(b.Get() + i, b.Length() - i); if (Wr <= 0) return false; i += Wr; } return true; } static GString FormEncode(const char *s, bool InValue = true) { GStringPipe p; for (auto c = s; *c; c++) { if (isalpha(*c) || isdigit(*c) || *c == '_' || *c == '.' || (!InValue && *c == '+') || *c == '-' || *c == '%') { p.Write(c, 1); } else if (*c == ' ') { p.Write((char*)"+", 1); } else { p.Print("%%%02.2X", *c); } } return p.NewGStr(); } struct LOAuth2Priv : public LCancel { LOAuth2::Params Params; GString Id; GStream *Log; GString Token; GString CodeVerifier; GStringPipe LocalLog; GDom *Store; GString AccessToken, RefreshToken; int64 ExpiresIn; struct Server : public GSocket { GSocket Listen; LOAuth2Priv *d; GSocket s; public: LHashTbl,GString> Params; GString Body; Server(LOAuth2Priv *cd) : d(cd) { while (!Listen.Listen(LOCALHOST_PORT)) { if (d->IsCancelled()) break; d->Log->Print("Error: Can't listen on %i...\n", LOCALHOST_PORT); LgiSleep(1000); } } bool GetReq() { while (!d->IsCancelled()) { if (Listen.IsReadable(100)) { if (Listen.Accept(&s)) { // Read access code out of response GString Hdrs; if (GetHttp(&s, Hdrs, Body, false)) { auto Url = UrlFromHeaders(Hdrs); auto Vars = Url.Split("?", 1); if (Vars.Length() != 2) { return false; } Vars = Vars[1].Split("&"); for (auto v : Vars) { auto p = v.Split("=", 1); if (p.Length() != 2) continue; Params.Add(p[0], p[1]); } return true; } } } } return false; } bool Response(const char *Txt) { GString Msg; Msg.Printf("HTTP/1.0 200 OK\r\n" "\r\n" "\n" "%s\n" "", Txt); return ::Write(&s, Msg); } }; GString Base64(GString s) { GString b; b.Length(BufferLen_BinTo64(s.Length())); auto ch = ConvertBinaryToBase64(b.Get(), b.Length(), (uchar*)s.Get(), s.Length()); b.Get()[b.Length()] = 0; return b; } GString ToText(GString Bin) { GArray t; for (char i='0'; i<='9'; i++) t.Add(i); for (char i='a'; i<='z'; i++) t.Add(i); for (char i='A'; i<='Z'; i++) t.Add(i); t.Add('-'); t.Add('.'); t.Add('_'); t.Add('~'); GString Txt; Txt.Length(Bin.Length()); int Pos = 0; for (int i=0; iPrint("Error: Can't connect to '%s:%i'\n", u.Host, HTTPS_PORT); return NULL; } GString Body, Http; Body.Printf("code=%s&" "client_id=%s&" "client_secret=%s&" "redirect_uri=http://localhost:%i&" "code_verifier=%s&" "grant_type=authorization_code", FormEncode(Token).Get(), Params.ClientID.Get(), Params.ClientSecret.Get(), LOCALHOST_PORT, FormEncode(CodeVerifier).Get()); - Http.Printf("POST /oauth2/v4/token HTTP/1.1\r\n" - "Host: www.googleapis.com\r\n" + GUri Api(Params.ApiUri); + Http.Printf("POST %s HTTP/1.1\r\n" + "Host: %s\r\n" "Content-Type: application/x-www-form-urlencoded\r\n" "Content-length: " LPrintfSizeT "\r\n" "\r\n" "%s", + Api.Path, + Api.Host, Body.Length(), Body.Get()); if (!Write(&sock, Http)) { Log->Print("%s:%i - Error writing to socket.\n", _FL); return false; } GString Hdrs; if (!GetHttp(&sock, Hdrs, Body, true)) { return false; } // Log->Print("Body=%s\n", Body.Get()); LJson j(Body); AccessToken = j.Get("access_token"); RefreshToken = j.Get("refresh_token"); ExpiresIn = j.Get("expires_in").Int(); } return AccessToken.Get() != NULL; } + bool Refresh() + { + if (!RefreshToken) + return false; + + GStringPipe p(1024); + GUri u(Params.Scope); + SslSocket sock(NULL, NULL, true); + if (!sock.Open(u.Host, HTTPS_PORT)) + { + Log->Print("Error: Can't connect to '%s:%i'\n", u.Host, HTTPS_PORT); + return NULL; + } + + GString Body, Http; + Body.Printf("refresh_token=%s&" + "client_id=%s&" + "client_secret=%s&" + "grant_type=refresh_token", + FormEncode(RefreshToken).Get(), + Params.ClientID.Get(), + Params.ClientSecret.Get()); + + GUri Api(Params.ApiUri); + Http.Printf("POST %s HTTP/1.1\r\n" + "Host: %s\r\n" + "Content-Type: application/x-www-form-urlencoded\r\n" + "Content-length: " LPrintfSizeT "\r\n" + "\r\n" + "%s", + Api.Path, + Api.Host, + Body.Length(), + Body.Get()); + if (!Write(&sock, Http)) + { + Log->Print("%s:%i - Error writing to socket.\n", _FL); + return false; + } + + GString Hdrs; + if (!GetHttp(&sock, Hdrs, Body, true)) + { + return false; + } + + Log->Print("Body=%s\n", Body.Get()); + LJson j(Body); + + AccessToken = j.Get("access_token"); + ExpiresIn = j.Get("expires_in").Int(); + + return AccessToken.Get() != NULL; + } + LOAuth2Priv(LOAuth2::Params ¶ms, const char *account, GDom *store, GStream *log) { Params = params; Id = account; Store = store; Log = log ? log : &LocalLog; } bool Serialize(bool Write) { if (!Store) return false; GVariant v; GString Key, kAccTok, kRefreshTok; Key.Printf("%s.%s", Params.Scope.Get(), Id.Get()); auto KeyB64 = Base64(Key); kAccTok.Printf("OAuth2-%s-%s", OPT_AccessToken, KeyB64.Get()); kAccTok = kAccTok.RStrip("="); kRefreshTok.Printf("OAuth2-%s-%s", OPT_RefreshToken, KeyB64.Get()); kRefreshTok= kRefreshTok.RStrip("="); if (Write) { Store->SetValue(kAccTok, v = AccessToken.Get()); Store->SetValue(kRefreshTok, v = RefreshToken.Get()); } else { if (Store->GetValue(kAccTok, v)) AccessToken = v.Str(); else return false; if (Store->GetValue(kRefreshTok, v)) RefreshToken = v.Str(); else return false; } return true; } }; LOAuth2::LOAuth2(LOAuth2::Params ¶ms, const char *account, GDom *store, GStream *log) { d = new LOAuth2Priv(params, account, store, log); d->Serialize(false); } LOAuth2::~LOAuth2() { d->Serialize(true); delete d; } bool LOAuth2::Refresh() { d->AccessToken.Empty(); d->Serialize(true); - return true; + return d->Refresh(); } GString LOAuth2::GetAccessToken() { if (d->AccessToken) return d->AccessToken; if (d->GetToken()) { d->Log->Print("Got token.\n"); if (d->GetAccess()) { return d->AccessToken; } } else d->Log->Print("No token.\n"); return GString(); } diff --git a/src/common/INet/MailImap.cpp b/src/common/INet/MailImap.cpp --- a/src/common/INet/MailImap.cpp +++ b/src/common/INet/MailImap.cpp @@ -1,3308 +1,3300 @@ #include #ifdef LINUX #include #endif #include "Lgi.h" #include "GToken.h" #include "Mail.h" #include "Base64.h" #include "INetTools.h" #include "GDocView.h" #include "IHttp.h" #include "HttpTools.h" #include "OpenSSLSocket.h" #include "LJson.h" #define DEBUG_OAUTH2 0 #ifdef _DEBUG #define DEBUG_FETCH 0 #else #define DEBUG_FETCH 0 #endif #define OPT_ImapOAuth2AccessToken "OAuth2AccessTok" #undef _FL #define _FL LgiGetLeaf(__FILE__), __LINE__ //////////////////////////////////////////////////////////////////////////// #if GPL_COMPATIBLE #include "AuthNtlm/Ntlm.h" #else #include "../../src/common/INet/libntlm-0.4.2/ntlm.h" #endif #if HAS_LIBGSASL #include "gsasl.h" #endif static const char *sRfc822Header = "RFC822.HEADER"; static const char *sRfc822Size = "RFC822.SIZE"; struct TraceLog : public GStream { ssize_t Read(void *Buffer, ssize_t Size, int Flags = 0) { return 0; } ssize_t Write(const void *Buffer, ssize_t Size, int Flags = 0) { LgiTrace("%.*s", (int)Size, Buffer); return Size; } }; /* #define SkipWhiteSpace(s) while (*s && IsWhiteSpace(*s)) s++; bool JsonDecode(GXmlTag &t, const char *s) { if (*s != '{') return false; s++; while (*s) { SkipWhiteSpace(s); if (*s != '\"') break; GAutoString Variable(LgiTokStr(s)); SkipWhiteSpace(s); if (*s != ':') return false; s++; SkipWhiteSpace(s); GAutoString Value(LgiTokStr(s)); SkipWhiteSpace(s); t.SetAttr(Variable, Value); if (*s != ',') break; s++; } if (*s != '}') return false; s++; return true; } */ #define SkipWhite(s) while (*s && strchr(WhiteSpace, *s)) s++ #define SkipSpaces(s) while (*s && strchr(" \t", *s)) s++ #define SkipNonWhite(s) while (*s && !strchr(WhiteSpace, *s)) s++; #define ExpectChar(ch) if (*s != ch) return 0; s++ ssize_t MailIMap::ParseImapResponse(char *Buffer, ssize_t BufferLen, GArray &Ranges, int Names) { Ranges.Length(0); if (!*Buffer || *Buffer != '*') return 0; char *End = Buffer + BufferLen; char *s = Buffer + 1; char *Start; for (int n=0; nOpen(u.Host, u.Port?u.Port:443)) return false; ssize_t w = S->Write(Req, ReqLen); if (w != ReqLen) return false; char Buf[256]; GArray Res; ssize_t r; ssize_t ContentLen = 0; ssize_t HdrLen = 0; while ((r = S->Read(Buf, sizeof(Buf))) > 0) { ssize_t Old = Res.Length(); Res.Length(Old + r); memcpy(&Res[Old], Buf, r); if (ContentLen) { if (Res.Length() >= HdrLen + ContentLen) break; } else { char *Eoh = strnstr(&Res[0], "\r\n\r\n", Res.Length()); if (Eoh) { HdrLen = Eoh - &Res[0]; GAutoString c(InetGetHeaderField(&Res[0], "Content-Length", HdrLen)); if (c) { ContentLen = atoi(c); } } } } char *Rp = &Res[0]; char *Eoh = strnstr(Rp, "\r\n\r\n", Res.Length()); if (Eoh) { if (OutHeaders) OutHeaders->Reset(NewStr(Rp, Eoh-Rp)); if (OutBody) OutBody->Reset(NewStr(Eoh + 4, Res.Length() - (Eoh-Rp) - 4)); if (StatusCode) { *StatusCode = 0; char *Eol = strchr(Rp, '\n'); if (Eol) { GToken t(Rp, " \t\r\n", true, Eol - Rp); if (t.Length() > 2) { *StatusCode = atoi(t[1]); } } } } else return false; #ifndef _DEBUG GFile f; if (f.Open("c:\\temp\\http.html", O_WRITE)) { f.SetSize(0); f.Write(&Res[0], Res.Length()); f.Close(); } #endif return true; } GAutoString ImapBasicTokenize(char *&s) { if (s) { while (*s && strchr(WhiteSpace, *s)) s++; char start = 0, end = 0; if (*s == '\'' || *s == '\"') start = end = *s; else if (*s == '[') { start = '['; end = ']'; } else if (*s == '(') { start = '('; end = ')'; } else if (*s == '{') { start = '{'; end = '}'; } if (start && end) { s++; char *e = strchr(s, end); if (e) { char *n = NewStr(s, e - s); s = e + 1; return GAutoString(n); } } else { char *e = s; while (*e && !strchr(WhiteSpace, *e)) e++; if (e > s) { char *n = NewStr(s, e - s); s = e + (*e != 0); return GAutoString(n); } } } s += strlen(s); return GAutoString(); } char *Tok(char *&s) { char *Ret = 0; while (*s && strchr(WhiteSpace, *s)) s++; if (*s == '=' || *s == ',') { Ret = NewStr(s++, 1); } else if (*s == '\'' || *s == '\"') { char d = *s++; char *e = strchr(s, d); if (e) { Ret = NewStr(s, e - s); s = e + 1; } } else if (*s) { char *e; for (e=s; *e && (IsDigit(*e) || IsAlpha(*e) || *e == '-'); e++); Ret = NewStr(s, e - s); s = e; } return Ret; } char *DecodeImapString(const char *s) { GStringPipe p; while (s && *s) { if (*s == '&') { char Escape = *s++; const char *e = s; while (*e && *e != '-') { e++; } ssize_t Len = e - s; if (Len) { char *Base64 = new char[Len + 4]; if (Base64) { memcpy(Base64, s, Len); char *n = Base64 + Len; for (ssize_t i=Len; i%4; i++) *n++ = '='; *n++ = 0; Len = strlen(Base64); ssize_t BinLen = BufferLen_64ToBin(Len); uint16 *Bin = new uint16[(BinLen/2)+1]; if (Bin) { BinLen = ConvertBase64ToBinary((uchar*)Bin, BinLen, Base64, Len); if (BinLen) { ssize_t Chars = BinLen / 2; Bin[Chars] = 0; for (int i=0; i>8) | ((Bin[i]&0xff)<<8); } char *c8 = WideToUtf8((char16*)Bin, BinLen); if (c8) { p.Push(c8); DeleteArray(c8); } } DeleteArray(Bin); } DeleteArray(Base64); } } else { p.Push(&Escape, 1); } s = e + 1; } else { p.Push(s, 1); s++; } } return p.NewStr(); } char *EncodeImapString(const char *s) { GStringPipe p; ssize_t Len = s ? strlen(s) : 0; while (s && *s) { int c = LgiUtf8To32((uint8*&)s, Len); DoNextChar: if ((c >= ' ' && c < 0x80) || c == '\n' || c == '\t' || c == '\r') { // Literal char ch = c; p.Push(&ch, 1); } else { // Encoded GArray Str; Str[0] = c; while ((c = LgiUtf8To32((uint8*&)s, Len))) { if ((c >= ' ' && c < 0x80) || c == '\n' || c == '\t' || c == '\r') { break; } else { Str[Str.Length()] = c; } } for (uint32 i=0; i>8) | ((Str[i]&0xff)<<8); } ssize_t BinLen = Str.Length() << 1; ssize_t BaseLen = BufferLen_BinTo64(BinLen); char *Base64 = new char[BaseLen+1]; if (Base64) { ssize_t Bytes = ConvertBinaryToBase64(Base64, BaseLen, (uchar*)&Str[0], BinLen); while (Bytes > 0 && Base64[Bytes-1] == '=') { Base64[Bytes-1] = 0; Bytes--; } Base64[Bytes] = 0; p.Print("&%s-", Base64); DeleteArray(Base64); } goto DoNextChar; } } return p.NewStr(); } void ChopNewLine(char *Str) { char *End = Str+strlen(Str)-1; if (*End == '\n') { *End-- = 0; } if (*End == '\r') { *End-- = 0; } } MailImapFolder::MailImapFolder() { Sep = '/'; Path = 0; NoSelect = false; NoInferiors = false; Marked = false; Exists = -1; Recent = -1; // UnseenIndex = -1; Deleted = 0; } MailImapFolder::~MailImapFolder() { DeleteArray(Path); } void MailImapFolder::operator =(LHashTbl,int> &v) { int o = v.Find("exists"); if (o >= 0) Exists = o; o = v.Find("recent"); if (o >= 0) Recent = o; } char *MailImapFolder::GetPath() { return Path; } void MailImapFolder::SetPath(const char *s) { char *NewPath = DecodeImapString(s); DeleteArray(Path); Path = NewPath; } char *MailImapFolder::GetName() { if (Path) { char *s = strrchr(Path, Sep); if (s) { return s + 1; } else { return Path; } } return 0; } void MailImapFolder::SetName(const char *s) { if (s) { char Buf[256]; strcpy_s(Buf, sizeof(Buf), Path?Path:(char*)""); DeleteArray(Path); char *Last = strrchr(Buf, Sep); if (Last) { Last++; strcpy_s(Last, sizeof(Buf)-(Last-Buf), s); Path = NewStr(Buf); } else { Path = NewStr(s); } } } ///////////////////////////////////////////// class MailIMapPrivate : public LMutex { public: int NextCmd; bool Logging; bool ExpungeOnExit; char FolderSep; char *Current; char *Flags; LHashTbl,bool> Capability; GString WebLoginUri; GViewI *ParentWnd; LCancel *Cancel; OsThread InCommand; GString LastWrite; MailIMapPrivate() : LMutex("MailImapSem") { ParentWnd = NULL; FolderSep = '/'; NextCmd = 1; Logging = true; ExpungeOnExit = true; Current = 0; Flags = 0; InCommand = 0; Cancel = NULL; } ~MailIMapPrivate() { DeleteArray(Current); DeleteArray(Flags); } }; MailIMap::MailIMap() { d = new MailIMapPrivate; Buffer[0] = 0; } MailIMap::~MailIMap() { if (Lock(_FL)) { ClearDialog(); ClearUid(); DeleteObj(d); } } bool MailIMap::Lock(const char *file, int line) { if (!d->Lock(file, line)) return false; return true; } bool MailIMap::LockWithTimeout(int Timeout, const char *file, int line) { if (!d->LockWithTimeout(Timeout, file, line)) return false; return true; } void MailIMap::Unlock() { d->Unlock(); d->InCommand = 0; } void MailIMap::SetCancel(LCancel *Cancel) { d->Cancel = Cancel; } void MailIMap::SetParentWindow(GViewI *wnd) { d->ParentWnd = wnd; } const char *MailIMap::GetWebLoginUri() { return d->WebLoginUri; } bool MailIMap::IsOnline() { return Socket ? Socket->IsOpen() : 0; } char MailIMap::GetFolderSep() { return d->FolderSep; } char *MailIMap::GetCurrentPath() { return d->Current; } bool MailIMap::GetExpungeOnExit() { return d->ExpungeOnExit; } void MailIMap::SetExpungeOnExit(bool b) { d->ExpungeOnExit = b; } void MailIMap::ClearUid() { if (Lock(_FL)) { Uid.DeleteArrays(); Unlock(); } } void MailIMap::ClearDialog() { if (Lock(_FL)) { Dialog.DeleteArrays(); Unlock(); } } bool MailIMap::WriteBuf(bool ObsurePass, const char *Buffer, bool Continuation) { if (Socket) { if (!Buffer) Buffer = Buf; ssize_t Len = strlen(Buffer); d->LastWrite = Buffer; if (!Continuation && d->InCommand) { GString Msg; Msg.Printf("%s:%i - WriteBuf failed(%s)\n", LgiGetLeaf(__FILE__), __LINE__, d->LastWrite.Strip().Get()); Socket->OnInformation(Msg); LgiAssert(!"Can't be issuing new commands while others are still running."); return false; } /* else { GString Msg; Msg.Printf("%s:%i - WriteBuf ok(%s)\n", LgiGetLeaf(__FILE__), __LINE__, d->LastWrite.Strip().Get()); Socket->OnInformation(Msg); } */ if (Socket->Write((void*)Buffer, Len, 0) == Len) { if (ObsurePass) { char *Sp = (char*)strrchr(Buffer, ' '); if (Sp) { Sp++; GString s; s.Printf("%.*s********\r\n", Sp - Buffer, Buffer); Log(s.Get(), GSocketI::SocketMsgSend); } } else Log(Buffer, GSocketI::SocketMsgSend); d->InCommand = LgiGetCurrentThread(); return true; } else Log("Failed to write data to socket.", GSocketI::SocketMsgError); } else Log("Not connected.", GSocketI::SocketMsgError); return false; } bool MailIMap::Read(GStreamI *Out, int Timeout) { int Lines = 0; while (!Lines && Socket) { ssize_t r = Socket->Read(Buffer, sizeof(Buffer)); if (Timeout > 0 && Socket->IsOpen() && r <= 0) { auto St = LgiCurrentTime(); auto Rd = Socket->IsReadable(Timeout); auto End = LgiCurrentTime(); if (Rd) { r = Socket->Read(Buffer, sizeof(Buffer)); if (r < 0) { Socket->Close(); LgiTrace("%s:%i - Wut? IsReadable/Read mismatch.\n", _FL); return false; } } else { if (End - St < Timeout - 20) LgiTrace("%s:%i - IsReadable broken (again)\n", _FL); return false; } } if (r > 0) { ReadBuf.Push(Buffer, r); while (ReadBuf.Pop(Buffer, sizeof(Buffer))) { // Trim trailing whitespace char *e = Buffer + strlen(Buffer) - 1; while (e > Buffer && strchr(WhiteSpace, *e)) *e-- = 0; Lines++; if (Out) { Out->Write(Buffer, strlen(Buffer)); Out->Write((char*)"\r\n", 2); } else { Dialog.Add(NewStr(Buffer)); } } } else break; } return Lines > 0; } bool MailIMap::IsResponse(const char *Buf, int Cmd, bool &Ok) { char Num[8]; int Ch = sprintf_s(Num, sizeof(Num), "A%4.4i ", Cmd); if (!Buf || _strnicmp(Buf, Num, Ch) != 0) return false; Ok = _strnicmp(Buf+Ch, "OK", 2) == 0; if (!Ok) SetError(L_ERROR_GENERIC, "Error: %s", Buf+Ch); return true; } bool MailIMap::ReadResponse(int Cmd, bool Plus) { bool Done = false; bool Status = false; if (Socket) { ssize_t Pos = Dialog.Length(); while (!Done) { if (Read(NULL)) { for (char *Dlg = Dialog[Pos]; !Done && Dlg; Dlg = Dialog.Next()) { Pos++; if (Cmd < 0 || (Plus && *Dlg == '+')) { Status = Done = true; } if (IsResponse(Dlg, Cmd, Status)) Done = true; if (d->Logging) { bool Good = strchr("*+", *Dlg) != NULL || Status; Log(Dlg, Good ? GSocketI::SocketMsgReceive : GSocketI::SocketMsgError); } } } else { // LgiTrace("%s:%i - 'Read' failed.\n", _FL); break; } } } return Status; } void Hex(char *Out, int OutLen, uchar *In, ssize_t InLen = -1) { if (Out && In) { if (InLen < 0) InLen = strlen((char*)In); for (int i=0; i 0) { Out += ch; OutLen -= ch; } else break; } } } void _unpack(void *ptr, int ptrsize, char *b64) { ConvertBase64ToBinary((uchar*) ptr, ptrsize, b64, strlen(b64)); } bool MailIMap::ReadLine() { int Len = 0; Buf[0] = 0; do { ssize_t r = Socket->Read(Buf+Len, sizeof(Buf)-Len); if (r < 1) return false; Len += r; } while (!stristr(Buf, "\r\n")); Log(Buf, GSocketI::SocketMsgReceive); return true; } #if HAS_LIBGSASL int GsaslCallback(Gsasl *ctx, Gsasl_session *sctx, Gsasl_property prop) { return 0; } #endif class OAuthWebServer : public LThread, public LMutex { bool Loop; int Port; GSocket Listen; GAutoString Req; GString Resp; bool Finished; public: OAuthWebServer(int DesiredPort = 0) : LThread("OAuthWebServerThread"), LMutex("OAuthWebServerMutex") { Loop = false; if (Listen.Listen(DesiredPort)) { Port = Listen.GetLocalPort(); Run(); } else Port = 0; Finished = false; } ~OAuthWebServer() { if (Loop) { Loop = false; while (!IsExited()) LgiSleep(10); } } int GetPort() { return Port; } GString GetRequest(LCancel *Loop, uint64 TimeoutMs = 0) { GString r; uint64 Start = LgiCurrentTime(); while (!r && (!Loop || !Loop->IsCancelled())) { if (Lock(_FL)) { if (Req) r = Req; Unlock(); } if (TimeoutMs) { uint64 Now = LgiCurrentTime(); if (Now - Start >= TimeoutMs) break; } if (!r) LgiSleep(50); } return r; } void SetResponse(const char *r) { if (Lock(_FL)) { Resp = r; Unlock(); } } bool IsFinished() { return Finished; } int Main() { GAutoPtr s; Loop = true; while (Loop) { if (Listen.CanAccept(100)) { s.Reset(new GSocket); if (!Listen.Accept(s)) s.Reset(); else { GArray Mem; ssize_t r; char buf[512]; do { r = s->Read(buf, sizeof(buf)); if (r > 0) { Mem.Add(buf, r); bool End = strnstr(&Mem[0], "\r\n\r\n", Mem.Length()) != NULL; if (End) break; } } while (r > 0); if (Lock(_FL)) { Mem.Add(0); Req.Reset(Mem.Release()); Unlock(); } // Wait for the response... GString Response; do { if (Lock(_FL)) { if (Resp) Response = Resp; Unlock(); } if (!Response) LgiSleep(10); } while (Loop && !Response); if (Response) s->Write(Response, Response.Length()); Loop = false; } } else LgiSleep(10); } Finished = true; return 0; } }; static void AddIfMissing(GArray &Auths, const char *a, GString *DefaultAuthType = NULL) { for (unsigned i=0; iLastWrite.Strip().Get()); Socket->OnInformation(Msg); */ d->InCommand = 0; d->LastWrite.Empty(); } bool MailIMap::Open(GSocketI *s, const char *RemoteHost, int Port, const char *User, const char *Password, GDom *settingStore, int Flags) { bool Status = false; if (settingStore) SettingStore = settingStore; if (SocketLock.Lock(_FL)) { Socket.Reset(s); SocketLock.Unlock(); } if (Socket && ValidStr(RemoteHost) && ValidStr(User) && ( ValidStr(Password) || OAuth2.IsValid() ) && Lock(_FL)) { // prepare address if (Port < 1) { if (Flags & MAIL_SSL) Port = IMAP_SSL_PORT; else Port = IMAP_PORT; } char Remote[256]; strcpy_s(Remote, sizeof(Remote), RemoteHost); char *Colon = strchr(Remote, ':'); if (Colon) { *Colon++ = 0; Port = atoi(Colon); } // Set SSL mode GVariant v; if (Flags == MAIL_SSL) v = "SSL"; Socket->SetValue(GSocket_Protocol, v); // connect if (Socket->Open(Remote, Port)) { bool IMAP4Server = false; GArray Auths; // check capability int CapCmd = d->NextCmd++; sprintf_s(Buf, sizeof(Buf), "A%4.4i CAPABILITY\r\n", CapCmd); if (WriteBuf()) { bool Rd = ReadResponse(CapCmd); CommandFinished(); if (Rd) { for (char *r=Dialog.First(); r; r=Dialog.Next()) { GToken T(r, " "); if (T.Length() > 1 && _stricmp(T[1], "CAPABILITY") == 0) { for (unsigned i=2; i 0) { Auths.DeleteAt(n); Auths.AddAt(0, DefaultAuthType); break; } } // SSL bool TlsError = false; if (TestFlag(Flags, MAIL_USE_STARTTLS)) { int CapCmd = d->NextCmd++; sprintf_s(Buf, sizeof(Buf), "A%4.4i STARTTLS\r\n", CapCmd); if (WriteBuf()) { bool Rd = ReadResponse(CapCmd); CommandFinished(); if (Rd) { GVariant v; TlsError = !Socket->SetValue(GSocket_Protocol, v="SSL"); } else { TlsError = true; } } else LgiAssert(0); if (TlsError) { Log("STARTTLS failed", GSocketI::SocketMsgError); } } // login bool LoggedIn = false; char AuthTypeStr[256] = ""; for (unsigned i=0; i 0) { strconcat(AuthTypeStr, ", "); } strconcat(AuthTypeStr, AuthType); } // Do auth #if HAS_LIBGSASL if (!_stricmp(AuthType, "GSSAPI")) { int AuthCmd = d->NextCmd++; sprintf_s(Buf, sizeof(Buf), "A%04.4i AUTHENTICATE GSSAPI\r\n", AuthCmd); if (WriteBuf() && ReadLine() && Buf[0] == '+') { // Start GSSAPI Gsasl *ctx = NULL; Gsasl_session *sess = NULL; int rc = gsasl_init(&ctx); if (rc == GSASL_OK) { char *mechs; rc = gsasl_client_mechlist(ctx, &mechs); gsasl_callback_set(ctx, GsaslCallback); rc = gsasl_client_start(ctx, AuthType, &sess); if (rc != GSASL_OK) { Log("gsasl_client_start failed", GSocketI::SocketMsgError); } // gsasl_step(ctx, gsasl_done(ctx); } else Log("gsasl_init failed", GSocketI::SocketMsgError); } else Log("AUTHENTICATE GSSAPI failed", GSocketI::SocketMsgError); } else #endif if (_stricmp(AuthType, "LOGIN") == 0 || _stricmp(AuthType, "OTP") == 0) { // clear text authentication int AuthCmd = d->NextCmd++; sprintf_s(Buf, sizeof(Buf), "A%4.4i LOGIN %s %s\r\n", AuthCmd, User, Password); if (WriteBuf(true)) { LoggedIn = ReadResponse(AuthCmd); CommandFinished(); } } else if (_stricmp(AuthType, "PLAIN") == 0) { // plain auth type char s[256]; char *e = s; *e++ = 0; strcpy_s(e, sizeof(s)-(e-s), User); e += strlen(e); e++; strcpy_s(e, sizeof(s)-(e-s), Password); e += strlen(e); *e++ = '\r'; *e++ = '\n'; ssize_t Len = e - s - 2; int AuthCmd = d->NextCmd++; sprintf_s(Buf, sizeof(Buf), "A%4.4i AUTHENTICATE PLAIN\r\n", AuthCmd); if (WriteBuf()) { if (ReadResponse(AuthCmd, true)) { ssize_t b = ConvertBinaryToBase64(Buf, sizeof(Buf), (uchar*)s, Len); strcpy_s(Buf+b, sizeof(Buf)-b, "\r\n"); if (WriteBuf(false, NULL, true)) { bool Rd = ReadResponse(AuthCmd); CommandFinished(); if (Rd) { LoggedIn = true; } else { // Look for WEBALERT from Google for (char *s = Dialog.First(); s; s = Dialog.Next()) { char *start = strchr(s, '['); char *end = start ? strrchr(start, ']') : NULL; if (start && end) { start++; if (_strnicmp(start, "WEBALERT", 8) == 0) { start += 8; while (*start && strchr(WhiteSpace, *start)) start++; d->WebLoginUri.Set(start, end - start); } } } } } } } } #if (GPL_COMPATIBLE || defined(_LIBNTLM_H)) && defined(WINNATIVE) else if (_stricmp(AuthType, "NTLM") == 0) { // NT Lan Man authentication OSVERSIONINFO ver; ZeroObj(ver); ver.dwOSVersionInfoSize = sizeof(ver); if (!GetVersionEx(&ver)) { DWORD err = GetLastError(); Log("Couldn't get OS version", GSocketI::SocketMsgError); } else { // Username is in the format: User[@Domain] char UserDom[256]; strcpy_s(UserDom, sizeof(UserDom), User); char *Domain = strchr(UserDom, '@'); if (Domain) *Domain++ = 0; int AuthCmd = d->NextCmd++; sprintf_s(Buf, sizeof(Buf), "A%04.4i AUTHENTICATE NTLM\r\n", AuthCmd); if (WriteBuf()) { if (ReadResponse(AuthCmd, true)) { tSmbNtlmAuthNegotiate negotiate; tSmbNtlmAuthChallenge challenge; tSmbNtlmAuthResponse response; buildSmbNtlmAuthNegotiate(&negotiate, 0, 0); if (NTLM_VER(&negotiate) == 2) { negotiate.v2.version.major = (uint8) ver.dwMajorVersion; negotiate.v2.version.minor = (uint8) ver.dwMinorVersion; negotiate.v2.version.buildNumber = (uint16) ver.dwBuildNumber; negotiate.v2.version.ntlmRevisionCurrent = 0x0f; } ZeroObj(Buf); int negotiateLen = SmbLength(&negotiate); int c = ConvertBinaryToBase64(Buf, sizeof(Buf), (uchar*)&negotiate, negotiateLen); strcpy_s(Buf+c, sizeof(Buf)-c, "\r\n"); WriteBuf(false, NULL, true); /* read challange data from server, convert from base64 */ Buf[0] = 0; ClearDialog(); if (ReadResponse()) { /* buffer should contain the string "+ [base 64 data]" */ #if 1 ZeroObj(challenge); char *Line = Dialog.First(); LgiAssert(Line != NULL); ChopNewLine(Line); int LineLen = strlen(Line); int challengeLen = sizeof(challenge); c = ConvertBase64ToBinary((uchar*) &challenge, sizeof(challenge), Line+2, LineLen-2); if (NTLM_VER(&challenge) == 2) challenge.v2.bufIndex = c - (challenge.v2.buffer-(uint8*)&challenge); else challenge.v1.bufIndex = c - (challenge.v1.buffer-(uint8*)&challenge); #endif /* prepare response, convert to base64, send to server */ ZeroObj(response); FILETIME time = {0, 0}; SYSTEMTIME stNow; GetSystemTime(&stNow); SystemTimeToFileTime(&stNow, &time); char HostName[256] = ""; gethostname(HostName, sizeof(HostName)); buildSmbNtlmAuthResponse(&challenge, &response, UserDom, HostName, Domain, Password, (uint8*)&time); if (NTLM_VER(&response) == 2) { response.v2.version.major = (uint8) ver.dwMajorVersion; response.v2.version.minor = (uint8) ver.dwMinorVersion; response.v2.version.buildNumber = (uint16) ver.dwBuildNumber; response.v2.version.ntlmRevisionCurrent = 0x0f; } #if 0 { uint8 *r1 = (uint8*)&response; uint8 *r2 = (uint8*)&response_good; for (int i=0; iNextCmd++; sprintf_s(Buf, sizeof(Buf), "A%4.4i AUTHENTICATE DIGEST-MD5\r\n", AuthCmd); if (WriteBuf()) { if (ReadResponse(AuthCmd)) { char *TestCnonce = 0; #if 0 // Test case strcpy(Buf, "+ cmVhbG09ImVsd29vZC5pbm5vc29mdC5jb20iLG5vbmNlPSJPQTZNRzl0RVFHbTJoaCIscW9wPSJhdXRoIixhbGdvcml0aG09bWQ1LXNlc3MsY2hhcnNldD11dGYtOA=="); RemoteHost = "elwood.innosoft.com"; User = "chris"; Password = "secret"; TestCnonce = "OA6MHXh6VqTrRk"; #endif char *In = (char*)Buf; if (In[0] == '+' && In[1] == ' ') { In += 2; uchar Out[2048]; ssize_t b = ConvertBase64ToBinary(Out, sizeof(Out), In, strlen(In)); Out[b] = 0; LHashTbl, char*> Map; char *s = (char*)Out; while (s && *s) { char *Var = Tok(s); char *Eq = Tok(s); char *Val = Tok(s); char *Comma = Tok(s); if (Var && Eq && Val && strcmp(Eq, "=") == 0) { Map.Add(Var, Val); Val = 0; } DeleteArray(Var); DeleteArray(Eq); DeleteArray(Val); DeleteArray(Comma); } int32 CnonceI[2] = { (int32)LgiRand(), (int32)LgiRand() }; char Cnonce[32]; if (TestCnonce) strcpy_s(Cnonce, sizeof(Cnonce), TestCnonce); else Cnonce[ConvertBinaryToBase64(Cnonce, sizeof(Cnonce), (uchar*)&CnonceI, sizeof(CnonceI))] = 0; s = strchr(Cnonce, '='); if (s) *s = 0; int Nc = 1; char *Realm = Map.Find("realm"); char DigestUri[256]; sprintf_s(DigestUri, sizeof(DigestUri), "imap/%s", Realm ? Realm : RemoteHost); GStringPipe p; p.Print("username=\"%s\"", User); p.Print(",nc=%08.8i", Nc); p.Print(",digest-uri=\"%s\"", DigestUri); p.Print(",cnonce=\"%s\"", Cnonce); char *Nonce = Map.Find("nonce"); if (Nonce) { p.Print(",nonce=\"%s\"", Nonce); } if (Realm) { p.Print(",realm=\"%s\"", Realm); } char *Charset = Map.Find("charset"); if (Charset) { p.Print(",charset=%s", Charset); } char *Qop = Map.Find("qop"); if (Qop) { p.Print(",qop=%s", Qop); } // Calculate A1 char a1[256]; uchar md5[16]; sprintf_s(Buf, sizeof(Buf), "%s:%s:%s", User, Realm ? Realm : (char*)"", Password); MDStringToDigest((uchar*)a1, Buf); char *Authzid = Map.Find("authzid"); int ch = 16; if (Authzid) ch += sprintf_s(a1+ch, sizeof(a1)-ch, ":%s:%s:%s", Nonce, Cnonce, Authzid); else ch += sprintf_s(a1+ch, sizeof(a1)-ch, ":%s:%s", Nonce, Cnonce); MDStringToDigest(md5, a1, ch); char a1hex[256]; Hex(a1hex, sizeof(a1hex), (uchar*)md5, sizeof(md5)); // Calculate char a2[256]; if (Qop && (_stricmp(Qop, "auth-int") == 0 || _stricmp(Qop, "auth-conf") == 0)) sprintf_s(a2, sizeof(a2), "AUTHENTICATE:%s:00000000000000000000000000000000", DigestUri); else sprintf_s(a2, sizeof(a2), "AUTHENTICATE:%s", DigestUri); MDStringToDigest(md5, a2); char a2hex[256]; Hex(a2hex, sizeof(a2hex), (uchar*)md5, sizeof(md5)); // Calculate the final response sprintf_s(Buf, sizeof(Buf), "%s:%s:%8.8i:%s:%s:%s", a1hex, Nonce, Nc, Cnonce, Qop, a2hex); MDStringToDigest(md5, Buf); Hex(Buf, sizeof(Buf), (uchar*)md5, sizeof(md5)); p.Print(",response=%s", Buf); if ((s = p.NewStr())) { ssize_t Chars = ConvertBinaryToBase64(Buf, sizeof(Buf) - 4, (uchar*)s, strlen(s)); LgiAssert(Chars < sizeof(Buf)); strcpy_s(Buf+Chars, sizeof(Buf)-Chars, "\r\n"); if (WriteBuf(false, NULL, true) && Read()) { for (char *Dlg = Dialog.First(); Dlg; Dlg=Dialog.Next()) { if (Dlg[0] == '+' && Dlg[1] == ' ') { Log(Dlg, GSocketI::SocketMsgReceive); strcpy_s(Buf, sizeof(Buf), "\r\n"); if (WriteBuf(false, NULL, true)) { LoggedIn = ReadResponse(AuthCmd); } } else { Log(Dlg, GSocketI::SocketMsgError); break; } } } DeleteArray(s); } } } CommandFinished(); } } else if (!_stricmp(AuthType, "XOAUTH2")) { if (stristr(RemoteHost, "office365.com")) { Log("office365.com doesn't support OAUTH2:", GSocketI::SocketMsgInfo); Log("\thttps://stackoverflow.com/questions/29747477/imap-auth-in-office-365-using-oauth2", GSocketI::SocketMsgInfo); Log("\tSo why does it report support in the CAPABILITY response? Don't ask me - fret", GSocketI::SocketMsgInfo); continue; } else if (!OAuth2.IsValid()) { sprintf_s(Buf, sizeof(Buf), "Error: Unknown OAUTH2 server '%s' (ask fret@memecode.com to add)", RemoteHost); Log(Buf, GSocketI::SocketMsgError); continue; } TraceLog TLog; int RefreshCount = 0; LOAuth2 Auth(OAuth2, User, SettingStore, &TLog); - RetryAccessToken: auto AccessToken = Auth.GetAccessToken(); if (!AccessToken) { sprintf_s(Buf, sizeof(Buf), "Warning: No OAUTH2 Access Token."); #if DEBUG_OAUTH2 LgiTrace("%s:%i - %s.\n", _FL, Buf); #endif Log(Buf, GSocketI::SocketMsgWarning); break; } // Construct the XOAUTH2 parameter GString s; s.Printf("user=%s\001auth=Bearer %s\001\001", User, AccessToken.Get()); #if DEBUG_OAUTH2 LgiTrace("%s:%i - s=%s.\n", _FL, s.Replace("\001", "%01").Get()); #endif Base64Str(s); // Issue the IMAP command int AuthCmd = d->NextCmd++; GString AuthStr; AuthStr.Printf("A%4.4i AUTHENTICATE XOAUTH2 %s\r\n", AuthCmd, s.Get()); if (WriteBuf(false, AuthStr)) { Dialog.DeleteArrays(); if (Read(NULL)) { for (char *l = Dialog.First(); l; l = Dialog.Next()) { if (*l == '+') { l++; while (*l && strchr(WhiteSpace, *l)) l++; s = l; UnBase64Str(s); Log(s, GSocketI::SocketMsgError); LJson t; t.SetJson(s); int StatusCode = (int)t.Get("status").Int(); LgiTrace("%s:%i - HTTP status: %i\n%s\n", _FL, StatusCode, s.Get()); sprintf_s(Buf, sizeof(Buf), "\r\n"); WriteBuf(false, NULL, true); if (StatusCode == 400) { // Refresh the token...? if (Auth.Refresh()) { - RefreshCount++; CommandFinished(); - if (RefreshCount < 2) - goto RetryAccessToken; + + // We need to restart the connection to use the refreshed token + // Seems we can't just re-try the authentication command. + return false; } } } else if (*l == '*') { Log(l, GSocketI::SocketMsgReceive); } else { if (IsResponse(l, AuthCmd, LoggedIn) && LoggedIn) { Log(l, GSocketI::SocketMsgReceive); if (SettingStore) { // Login successful, so persist the AuthCode for next time GVariant v = AccessToken.Get(); bool b = SettingStore->SetValue(OPT_ImapOAuth2AccessToken, v); if (!b) { Log("Couldn't store access token.", GSocketI::SocketMsgWarning); } } break; } else { Log(l, GSocketI::SocketMsgError); } } } } CommandFinished(); } - - if (!LoggedIn && SettingStore) - { - GVariant v; - SettingStore->SetValue(OPT_ImapOAuth2AccessToken, v); - break; - } - } else { char s[256]; sprintf_s(s, sizeof(s), "Warning: Unsupported authentication type '%s'", AuthType); Log(s, GSocketI::SocketMsgWarning); } } if (LoggedIn) { Status = true; // Ask server for it's heirarchy (folder) separator. int Cmd = d->NextCmd++; sprintf_s(Buf, sizeof(Buf), "A%4.4i LIST \"\" \"\"\r\n", Cmd); if (WriteBuf()) { ClearDialog(); Buf[0] = 0; if (ReadResponse(Cmd)) { for (char *Dlg = Dialog.First(); Dlg; Dlg=Dialog.Next()) { GArray t; char *s = Dlg; while (*s) { GAutoString a = ImapBasicTokenize(s); if (a) t.New() = a; else break; } if (t.Length() >= 5 && strcmp(t[0], "*") == 0 && _stricmp(t[1], "list") == 0) { for (unsigned i=2; iFolderSep = *s; break; } } break; } } } CommandFinished(); } } else { SetError(L_ERROR_UNSUPPORTED_AUTH, "Authentication failed, types available:\n\t%s", ValidStr(AuthTypeStr) ? AuthTypeStr : "(none)"); } } } Unlock(); } return Status; } bool MailIMap::Close() { bool Status = false; if (Socket && Lock(_FL)) { if (d->ExpungeOnExit) { ExpungeFolder(); } int Cmd = d->NextCmd++; sprintf_s(Buf, sizeof(Buf), "A%4.4i LOGOUT\r\n", Cmd); if (WriteBuf()) { Status = true; } CommandFinished(); Unlock(); } return Status; } bool MailIMap::GetCapabilities(GArray &s) { // char *k = 0; // for (bool p=d->Capability.First(&k); p; p=d->Capability.Next(&k)) for (auto i : d->Capability) { s.Add(i.key); } return s.Length() > 0; } bool MailIMap::ServerOption(char *Opt) { return d->Capability.Find(Opt) != 0; } char *MailIMap::GetSelectedFolder() { return d->Current; } bool MailIMap::SelectFolder(const char *Path, StrMap *Values) { bool Status = false; if (Socket && Lock(_FL)) { int Cmd = d->NextCmd++; char *Enc = EncodePath(Path); sprintf_s(Buf, sizeof(Buf), "A%4.4i SELECT \"%s\"\r\n", Cmd, Enc); DeleteArray(Enc); if (WriteBuf()) { DeleteArray(d->Current); ClearDialog(); if (ReadResponse(Cmd)) { Uid.DeleteArrays(); if (Values) { for (GString Dlg = Dialog.First(); Dlg; Dlg = Dialog.Next()) { GString::Array t = Dlg.SplitDelimit(" []"); if (t.Length() > 0 && t[0].Equals("*")) { for (unsigned i=1; iAdd(t[i], t[i-1]); } else if (t[i].Equals("unseen")) { //char *val = t[i+1]; if (t[i+1].IsNumeric()) Values->Add(t[i], t[i+1]); } else if (t[i].Equals("flags")) { ssize_t s = Dlg.Find("("); ssize_t e = Dlg.Find(")", s + 1); if (e >= 0) { GString Val = Dlg(s+1, e); Values->Add(t[i], Val); } } } } } } Status = true; d->Current = NewStr(Path); ClearDialog(); } CommandFinished(); } Unlock(); } return Status; } int MailIMap::GetMessages(const char *Path) { int Status = 0; if (Socket && Lock(_FL)) { StrMap f; if (SelectFolder(Path, &f)) { GString Exists = f.Find("exists"); if (Exists && Exists.Int() >= 0) Status = (int)Exists.Int(); else LgiTrace("%s:%i - Failed to get 'exists' value.\n", _FL); } Unlock(); } return Status; } int MailIMap::GetMessages() { return GetMessages("INBOX"); } char *MailIMap::SequenceToString(GArray *Seq) { if (!Seq) return NewStr("1:*"); GStringPipe p; // int Last = 0; for (unsigned s=0; sLength(); ) { unsigned e = s; while (eLength()-1 && (*Seq)[e] == (*Seq)[e+1]-1) e++; if (s) p.Print(","); if (e == s) p.Print("%i", (*Seq)[s]); else p.Print("%i:%i", (*Seq)[s], (*Seq)[e]); s = e + 1; } return p.NewStr(); } static void RemoveBytes(GArray &a, ssize_t &Used, ssize_t Bytes) { if (Used >= Bytes) { ssize_t Remain = Used - Bytes; if (Remain > 0) memmove(&a[0], &a[Bytes], Remain); Used -= Bytes; } else LgiAssert(0); } static bool PopLine(GArray &a, ssize_t &Used, GAutoString &Line) { for (ssize_t i=0; iNextCmd++; GStringPipe p(256); p.Print("A%4.4i %sFETCH ", Cmd, ByUid ? "UID " : ""); p.Write(Seq, strlen(Seq)); p.Print(" (%s)\r\n", Parts); GAutoString WrBuf(p.NewStr()); if (WriteBuf(false, WrBuf)) { ClearDialog(); GArray Buf; Buf.Length(1024 + (SizeHint>0?(uint32)SizeHint:0)); ssize_t Used = 0; ssize_t MsgSize; // int64 Start = LgiCurrentTime(); int64 Bytes = 0; bool Done = false; // uint64 TotalTs = 0; bool Blocking = Socket->IsBlocking(); Socket->IsBlocking(false); #if DEBUG_FETCH LgiTrace("%s:%i - Fetch: Starting loop\n", _FL); #endif uint64 LastActivity = LgiCurrentTime(); bool Debug = false; while (!Done && Socket->IsOpen()) { ssize_t r; // We don't wait for 'readable' with select here because // a) The socket is in non-blocking mode and // b) For OpenSSL connections 'readable' on the socket != can get bytes from 'read'. // Just try the read first and see if it gives you bytes, if not then 'select' on the socket. while (true) { // Extend the buffer if getting used up if (Buf.Length()-Used <= 256) { Buf.Length(Buf.Length() + (64<<10)); #if DEBUG_FETCH LgiTrace("%s:%i - Fetch: Ext buf: %i\n", _FL, Buf.Length()); #endif } // Try and read bytes from server. r = Socket->Read(Buf.AddressOf(Used), Buf.Length()-Used-1); // -1 for NULL terminator #if DEBUG_FETCH LgiTrace("%s:%i - Fetch: r=%i, used=%i, buf=%i\n", _FL, r, Used, Buf.Length()); #endif if (r > 0) { if (RawCopy) RawCopy->Write(&Buf[Used], r); Used += r; Bytes += r; LastActivity = LgiCurrentTime(); } else { LgiSleep(1); // Don't eat the whole CPU... break; } if (Debug) LgiTrace("%s:%i - Recv=%i\n", _FL, r); } // See if we can parse out a single response GArray Ranges; LgiAssert(Used < Buf.Length()); Buf[Used] = 0; // NULL terminate before we parse while (true) { MsgSize = ParseImapResponse(&Buf[0], Used, Ranges, 2); #if DEBUG_FETCH LgiTrace("%s:%i - Fetch: MsgSize=%i\n", _FL, MsgSize); #endif if (Debug) LgiTrace("%s:%i - ParseImapResponse=%i\n", _FL, MsgSize); if (!MsgSize) break; if (!Debug) LastActivity = LgiCurrentTime(); char *b = &Buf[0]; if (MsgSize > Used) { // This is an error... ParseImapResponse should always return <= Used. // If this triggers, ParseImapResponse is skipping a NULL that it shouldn't. #if DEBUG_FETCH LgiTrace("%s:%i - Fetch: Wrong size %i, %i\n", _FL, MsgSize, Used); #endif Ranges.Length(0); LgiAssert(0); #if _DEBUG ParseImapResponse(&Buf[0], Used, Ranges, 2); #endif Done = true; break; } LgiAssert(Ranges.Length() >= 2); // Setup strings for callback char *Param = b + Ranges[0].Start; Param[Ranges[0].Len()] = 0; char *Name = b + Ranges[1].Start; Name[Ranges[1].Len()] = 0; if (_stricmp(Name, "FETCH")) { // Not the response we're looking for. #if DEBUG_FETCH LgiTrace("%s:%i - Fetch: Wrong response: %s\n", _FL, Name); #endif } else { // Process ranges into a hash table StrMap Parts; for (unsigned i=2; i 0 && Buf[0] != '*') { GAutoString Line; while (PopLine(Buf, Used, Line)) { #if DEBUG_FETCH LgiTrace("%s:%i - Fetch: Line='%s'\n", _FL, Line.Get()); #endif GToken t(Line, " \r\n"); if (t.Length() >= 2) { char *r = t[0]; if (*r == 'A') { bool IsOk = !_stricmp(t[1], "Ok"); int Response = atoi(r + 1); Log(Line, IsOk ? GSocketI::SocketMsgReceive : GSocketI::SocketMsgError); if (Response == Cmd) { Done = true; break; } } else Log(&Buf[0], GSocketI::SocketMsgError); } else { // This is normal behaviour... just don't have the end marker yet. Done = true; break; } } } } Socket->IsBlocking(Blocking); CommandFinished(); #if DEBUG_FETCH LgiTrace("%s:%i - Fetch finished, status=%i\n", _FL, Status); #endif } Unlock(); return Status; } bool IMapHeadersCallback(MailIMap *Imap, char *Msg, MailIMap::StrMap &Parts, void *UserData) { char *s = Parts.Find(sRfc822Header); if (s) { Parts.Delete(sRfc822Header); GAutoString *Hdrs = (GAutoString*)UserData; Hdrs->Reset(s); } return true; } char *MailIMap::GetHeaders(int Message) { GAutoString Text; if (Lock(_FL)) { char Seq[64]; sprintf_s(Seq, sizeof(Seq), "%i", Message + 1); Fetch( false, Seq, sRfc822Header, IMapHeadersCallback, &Text, NULL); Unlock(); } return Text.Release(); } struct ReceiveCallbackState { MailTransaction *Trans; MailCallbacks *Callbacks; }; static bool IMapReceiveCallback(MailIMap *Imap, char *Msg, MailIMap::StrMap &Parts, void *UserData) { ReceiveCallbackState *State = (ReceiveCallbackState*) UserData; char *Flags = Parts.Find("FLAGS"); if (Flags) { State->Trans->Imap.Set(Flags); } char *Hdrs = Parts.Find(sRfc822Header); if (Hdrs) { ssize_t Len = strlen(Hdrs); State->Trans->Stream->Write(Hdrs, Len); } char *Body = Parts.Find("BODY[TEXT]"); if (Body) { ssize_t Len = strlen(Body); State->Trans->Stream->Write(Body, Len); } State->Trans->Status = Hdrs != NULL || Body != NULL; if (Imap->Items) Imap->Items->Value++; Parts.Empty(); if (State->Callbacks) { bool Ret = State->Callbacks->OnReceive(State->Trans, State->Callbacks->CallbackData); if (!Ret) return false; } return true; } bool MailIMap::Receive(GArray &Trans, MailCallbacks *Callbacks) { bool Status = false; if (Lock(_FL)) { int Errors = 0; ReceiveCallbackState State; State.Callbacks = Callbacks; for (unsigned i=0; iStatus = false; char Seq[64]; sprintf_s(Seq, sizeof(Seq), "%i", State.Trans->Index + 1); Fetch ( false, Seq, "FLAGS RFC822.HEADER BODY[TEXT]", IMapReceiveCallback, &State, NULL ); if (State.Trans->Status) { Status = true; } else if (Errors++ > 5) { // Yeah... not feelin' it Status = false; break; } } Unlock(); } return Status; } bool MailIMap::Append(const char *Folder, ImapMailFlags *Flags, const char *Msg, GString &NewUid) { bool Status = false; if (Folder && Msg && Lock(_FL)) { GAutoString Flag(Flags ? Flags->Get() : NULL); GAutoString Path(EncodePath(Folder)); int Cmd = d->NextCmd++; int Len = 0; for (const char *m = Msg; *m; m++) { if (*m == '\n') { Len += 2; } else if (*m != '\r') { Len++; } } // Append on the end of the mailbox int c = sprintf_s(Buf, sizeof(Buf), "A%4.4i APPEND \"%s\"", Cmd, Path.Get()); if (Flag) c += sprintf_s(Buf+c, sizeof(Buf)-c, " (%s)", Flag.Get()); c += sprintf_s(Buf+c, sizeof(Buf)-c, " {%i}\r\n", Len); if (WriteBuf()) { if (Read()) { bool GotPlus = false; for (char *Dlg = Dialog.First(); Dlg; Dlg = Dialog.Next()) { if (Dlg[0] == '+') { Dialog.Delete(Dlg); DeleteArray(Dlg); GotPlus = true; break; } } if (GotPlus) { int Wrote = 0; for (const char *m = Msg; *m; ) { while (*m == '\r' || *m == '\n') { if (*m == '\n') { Wrote += Socket->Write((char*)"\r\n", 2); } m++; } const char *e = m; while (*e && *e != '\r' && *e != '\n') e++; if (e > m) { Wrote += Socket->Write(m, e-m); m = e; } else break; } LgiAssert(Wrote == Len); Wrote += Socket->Write((char*)"\r\n", 2); // Read response.. ClearDialog(); if ((Status = ReadResponse(Cmd))) { char Tmp[16]; sprintf_s(Tmp, sizeof(Tmp), "A%4.4i", Cmd); for (char *Line = Dialog.First(); Line; Line = Dialog.Next()) { GAutoString c = ImapBasicTokenize(Line); if (!c) break; if (!strcmp(Tmp, c)) { GAutoString a; while ((a = ImapBasicTokenize(Line)).Get()) { GToken t(a, " "); if (t.Length() > 2 && !_stricmp(t[0], "APPENDUID")) { NewUid = t[2]; break; } } } } } } } CommandFinished(); } Unlock(); } return Status; } bool MailIMap::Delete(int Message) { bool Status = false; if (Socket && Lock(_FL)) { int Cmd = d->NextCmd++; sprintf_s(Buf, sizeof(Buf), "A%4.4i STORE %i FLAGS (\\deleted)\r\n", Cmd, Message+1); if (WriteBuf()) { ClearDialog(); Status = ReadResponse(Cmd); CommandFinished(); } Unlock(); } return Status; } bool MailIMap::Delete(bool ByUid, const char *Seq) { bool Status = false; if (Socket && Lock(_FL)) { int Cmd = d->NextCmd++; sprintf_s(Buf, sizeof(Buf), "A%4.4i %sSTORE %s FLAGS (\\deleted)\r\n", Cmd, ByUid?"UID ":"", Seq); if (WriteBuf()) { ClearDialog(); Status = ReadResponse(Cmd); CommandFinished(); } Unlock(); } return Status; } int MailIMap::Sizeof(int Message) { int Status = 0; if (Socket && Lock(_FL)) { int Cmd = d->NextCmd++; sprintf_s(Buf, sizeof(Buf), "A%4.4i FETCH %i (%s)\r\n", Cmd, Message+1, sRfc822Size); if (WriteBuf()) { ClearDialog(); Buf[0] = 0; if (ReadResponse(Cmd)) { char *d = Dialog.First(); if (d) { char *t = strstr(d, sRfc822Size); if (t) { t += strlen(sRfc822Size) + 1; Status = atoi(t); } } } CommandFinished(); } Unlock(); } return Status; } bool ImapSizeCallback(MailIMap *Imap, char *Msg, MailIMap::StrMap &Parts, void *UserData) { GArray *Sizes = (GArray*) UserData; int Index = atoi(Msg); if (Index < 1) return false; char *Sz = Parts.Find(sRfc822Size); if (!Sz) return false; (*Sizes)[Index - 1] = atoi(Sz); return true; } bool MailIMap::GetSizes(GArray &Sizes) { return Fetch(false, "1:*", sRfc822Size, ImapSizeCallback, &Sizes) != 0; } bool MailIMap::GetUid(int Message, char *Id, int IdLen) { bool Status = false; if (Lock(_FL)) { if (FillUidList()) { char *s = Uid.ItemAt(Message); if (s && Id) { strcpy_s(Id, IdLen, s); Status = true; } } Unlock(); } return Status; } bool MailIMap::FillUidList() { bool Status = false; if (Socket && Lock(_FL)) { if (Uid.Length() == 0) { int Cmd = d->NextCmd++; sprintf_s(Buf, sizeof(Buf), "A%4.4i UID SEARCH ALL\r\n", Cmd); if (WriteBuf()) { ClearDialog(); if (ReadResponse(Cmd)) { for (char *d = Dialog.First(); d && !Status; d=Dialog.Next()) { GToken T(d, " "); if (T[1] && strcmp(T[1], "SEARCH") == 0) { for (unsigned i=2; i &Id) { bool Status = false; if (Lock(_FL)) { if (FillUidList()) { for (char *s=Uid.First(); s; s=Uid.Next()) { Id.Insert(NewStr(s)); } Status = true; } Unlock(); } return Status; } bool MailIMap::GetFolders(GArray &Folders) { bool Status = false; if (Socket && Lock(_FL)) { int Cmd = d->NextCmd++; sprintf_s(Buf, sizeof(Buf), "A%4.4i LIST \"\" \"*\"\r\n", Cmd); if (WriteBuf()) { ClearDialog(); Buf[0] = 0; if (ReadResponse(Cmd)) { char Sep[] = { GetFolderSep(), 0 }; for (char *d = Dialog.First(); d; d=Dialog.Next()) { GArray t; char *s; while ((s = LgiTokStr((const char*&)d))) { t[t.Length()].Reset(s); } if (t.Length() >= 5) { if (strcmp(t[0], "*") == 0 && _stricmp(t[1], "LIST") == 0) { char *Folder = t[t.Length()-1]; MailImapFolder *f = new MailImapFolder(); if (f) { Folders.Add(f); f->Sep = Sep[0]; // Check flags f->NoSelect = stristr(t[2], "NoSelect") != 0; f->NoInferiors = stristr(t[2], "NoInferiors") != 0; // LgiTrace("Imap folder '%s' %s\n", Folder, t[2].Get()); // Alloc name if (Folder[0] == '\"') { char *p = TrimStr(Folder, "\""); f->Path = DecodeImapString(p); DeleteArray(p); } else { f->Path = DecodeImapString(Folder); } } } } } Status = true; ClearDialog(); } CommandFinished(); } Unlock(); } return Status; } bool MailIMap::CreateFolder(MailImapFolder *f) { bool Status = false; // char Dir[2] = { d->FolderSep, 0 }; if (f && f->GetPath() && Lock(_FL)) { int Cmd = d->NextCmd++; char *Enc = EncodePath(f->GetPath()); sprintf_s(Buf, sizeof(Buf), "A%4.4i CREATE \"%s\"\r\n", Cmd, Enc); DeleteArray(Enc); if (WriteBuf()) { ClearDialog(); Status = ReadResponse(Cmd); if (Status) { char *End = f->Path + strlen(f->Path) - 1; if (*End == GetFolderSep()) { f->NoSelect = true; *End = 0; } else { f->NoInferiors = true; } } CommandFinished(); } Unlock(); } return Status; } char *MailIMap::EncodePath(const char *Path) { if (!Path) return 0; char Sep = GetFolderSep(); char Native[MAX_PATH], *o = Native, *e = Native + sizeof(Native) - 1; for (const char *i = *Path == '/' ? Path + 1 : Path; *i && o < e; i++) { if (*i == '/') *o++ = Sep; else *o++ = *i; } *o++ = 0; return EncodeImapString(Native); } bool MailIMap::DeleteFolder(const char *Path) { bool Status = false; if (Path && Lock(_FL)) { // Close the current folder if required. if (d->Current && _stricmp(Path, d->Current) == 0) { int Cmd = d->NextCmd++; sprintf_s(Buf, sizeof(Buf), "A%4.4i CLOSE\r\n", Cmd); if (WriteBuf()) { ClearDialog(); ReadResponse(Cmd); DeleteArray(d->Current); CommandFinished(); } } // Delete the folder int Cmd = d->NextCmd++; char *NativePath = EncodePath(Path); sprintf_s(Buf, sizeof(Buf), "A%4.4i DELETE \"%s\"\r\n", Cmd, NativePath); DeleteArray(NativePath); if (WriteBuf()) { ClearDialog(); Status = ReadResponse(Cmd); CommandFinished(); } Unlock(); } return Status; } bool MailIMap::RenameFolder(const char *From, const char *To) { bool Status = false; if (From && To && Lock(_FL)) { int Cmd = d->NextCmd++; GAutoString f(EncodePath(From)); GAutoString t(EncodePath(To)); sprintf_s(Buf, sizeof(Buf), "A%4.4i RENAME \"%s\" \"%s\"\r\n", Cmd, f.Get(), t.Get()); if (WriteBuf()) { ClearDialog(); Status = ReadResponse(Cmd); CommandFinished(); } Unlock(); } return Status; } bool MailIMap::SetFolderFlags(MailImapFolder *f) { bool Status = false; if (f && Lock(_FL)) { /* int Cmd = d->NextCmd++; sprintf_s(Buf, sizeof(Buf), "A%4.4i RENAME \"%s\" \"%s\"\r\n", Cmd); if (WriteBuf()) { ClearDialog(); Status = ReadResponse(Cmd); } */ Unlock(); } return Status; } bool MailIMap::SetFlagsByUid(GArray &Uids, const char *Flags) { bool Status = false; if (Lock(_FL)) { int Cmd = d->NextCmd++; GStringPipe p; p.Print("A%04.4i UID STORE ", Cmd); if (Uids.Length()) { for (unsigned i=0; i &InUids, const char *DestFolder) { bool Status = false; if (Lock(_FL)) { int Cmd = d->NextCmd++; GAutoString Dest(EncodePath(DestFolder)); GStringPipe p(1024); p.Print("A%04.4i UID COPY ", Cmd); for (unsigned i=0; iNextCmd++; sprintf_s(Buf, sizeof(Buf), "A%4.4i EXPUNGE\r\n", Cmd); if (WriteBuf()) { ClearDialog(); Status = ReadResponse(Cmd); CommandFinished(); } Unlock(); } return Status; } bool MailIMap::Search(bool Uids, GArray &SeqNumbers, const char *Filter) { bool Status = false; if (ValidStr(Filter) && Lock(_FL)) { int Cmd = d->NextCmd++; sprintf_s(Buf, sizeof(Buf), "A%4.4i %sSEARCH %s\r\n", Cmd, Uids?"UID ":"", Filter); if (WriteBuf()) { ClearDialog(); if (ReadResponse(Cmd)) { for (char *d = Dialog.First(); d; d = Dialog.Next()) { if (*d != '*') continue; d++; GAutoString s(Tok(d)); if (!s || _stricmp(s, "Search")) continue; while (s.Reset(Tok(d))) { SeqNumbers.New() = s; Status = true; } } } CommandFinished(); } Unlock(); } return Status; } bool MailIMap::Status(char *Path, int *Recent) { bool Status = false; if (Path && Recent && Lock(_FL)) { GAutoString Dest(EncodePath(Path)); int Cmd = d->NextCmd++; sprintf_s(Buf, sizeof(Buf), "A%4.4i STATUS %s (RECENT)\r\n", Cmd, Dest.Get()); if (WriteBuf()) { ClearDialog(); if (ReadResponse(Cmd)) { for (char *d=Dialog.First(); d; d=Dialog.Next()) { if (*d != '*') continue; d++; GAutoString Cmd = ImapBasicTokenize(d); GAutoString Folder = ImapBasicTokenize(d); GAutoString Fields = ImapBasicTokenize(d); if (Cmd && Folder && Fields && !_stricmp(Cmd, "status") && !_stricmp(Folder, Dest)) { char *f = Fields; GAutoString Field = ImapBasicTokenize(f); GAutoString Value = ImapBasicTokenize(f); if (Field && Value && !_stricmp(Field, "recent")) { *Recent = atoi(Value); Status = true; break; } } } } else LgiTrace("%s:%i - STATUS cmd failed.\n", _FL); CommandFinished(); } Unlock(); } return Status; } bool MailIMap::Poll(int *Recent, GArray *New) { bool Status = true; if (Lock(_FL)) { int Cmd = d->NextCmd++; sprintf_s(Buf, sizeof(Buf), "A%4.4i NOOP\r\n", Cmd); if (WriteBuf()) { ClearDialog(); if ((Status = ReadResponse(Cmd))) { int LocalRecent; if (!Recent) Recent = &LocalRecent; *Recent = 0; for (char *Dlg=Dialog.First(); Dlg; Dlg=Dialog.Next()) { if (Recent && stristr(Dlg, " RECENT")) { *Recent = atoi(Dlg + 2); } } if (*Recent && New) { Search(false, *New, "new"); } } CommandFinished(); } Unlock(); } return Status; } bool MailIMap::StartIdle() { bool Status = false; if (Lock(_FL)) { int Cmd = d->NextCmd++; sprintf_s(Buf, sizeof(Buf), "A%4.4i IDLE\r\n", Cmd); Status = WriteBuf(); CommandFinished(); Unlock(); } return Status; } bool MailIMap::OnIdle(int Timeout, GArray &Resp) { bool Status = false; if (Lock(_FL)) { auto Blk = Socket->IsBlocking(); Socket->IsBlocking(false); #if 0 // def _DEBUG auto Start = LgiCurrentTime(); #endif Read(NULL, Timeout); #if 0 // def _DEBUG auto Time = LgiCurrentTime() - Start; if (Timeout > 0 && Time < (uint64)(Timeout * 0.9)) { printf("Short rd " LPrintfInt64 " of %i\n", Time, Timeout); } #endif Socket->IsBlocking(Blk); char *Dlg; while ((Dlg = Dialog.First())) { Dialog.Delete(Dlg); Log(Dlg, GSocketI::SocketMsgReceive); if (Dlg[0] == '*' && Dlg[1] == ' ') { char *s = Dlg + 2; GAutoString a = ImapBasicTokenize(s); GAutoString b = ImapBasicTokenize(s); if (a && b) { Untagged &u = Resp.New(); if (IsDigit(a[0])) { u.Cmd = b.Get(); u.Id = atoi(a); if (ValidStr(s)) u.Param = s; } else { u.Param = Dlg + 2; } Status = true; } } DeleteArray(Dlg); } Unlock(); } return Status; } bool MailIMap::FinishIdle() { bool Status = false; if (Lock(_FL)) { if (WriteBuf(false, "DONE\r\n")) { Status = ReadResponse(); CommandFinished(); } Unlock(); } return Status; }