diff --git a/Code/AddressSelect.cpp b/Code/AddressSelect.cpp --- a/Code/AddressSelect.cpp +++ b/Code/AddressSelect.cpp @@ -1,626 +1,626 @@ #include "Scribe.h" #include "lgi/common/Edit.h" #include "resdefs.h" #include "lgi/common/ClipBoard.h" #include "lgi/common/DisplayString.h" #include "ScribeListAddr.h" #include "lgi/common/LgiRes.h" #include "AddressSelect.h" /////////////////////////////////////////////////////////////////////////////////////// static const char *Empty = ""; AddressList::AddressList(ScribeWnd *app, int id, int x, int y, int cx, int cy, const char *name) : LList(id, x, y, cx, cy, name) { App = app; _ObjName = Res_Custom; ColumnHeaders = false; } int AddressList::WillAccept(LDragFormats &Formats, LPoint Pt, int KeyState) { if (Formats.HasFormat(ScribeThingList)) Formats.Supports(ScribeThingList); else Formats.SupportsFileDrops(); return Formats.GetSupported().Length() ? DROPEFFECT_COPY : DROPEFFECT_NONE; } int AddressList::OnDrop(LArray &Data, LPoint Pt, int KeyState) { int Status = DROPEFFECT_NONE; for (unsigned idx=0; idx 0 && dd.Data[0].Type == GV_BINARY && ScribeClipboardFmt::IsThing(dd.Data[0].Value.Binary.Data, dd.Data[0].Value.Binary.Length)) { ScribeClipboardFmt *tl = (ScribeClipboardFmt*)dd.Data[0].Value.Binary.Data; for (uint32_t i=0; iLength(); i++) { Contact *c = tl->ThingAt(i)->IsContact(); if (c) { ListAddr *New = new ListAddr(c); if (New) Insert(New, -1, false); } } Invalidate(); Status = DROPEFFECT_COPY; } } else if (_stricmp(dd.Format, LGI_FileDropFormat) == 0) { } } return DROPEFFECT_NONE; } void AddressList::OnCreate() { LDisplayString ds(LSysFont, "Bcc:"); AddColumn("To", ds.X() + 2); AddColumn("Info", 1000); SetWindow(this); } -void AddressList::OnInit(GDataIt l) +void AddressList::OnInit(LDataIt l) { Empty(); for (LDataPropI *a = l->First(); a; a = l->Next()) { ListAddr *New = new ListAddr(App, a); if (New) { New->begin(); Insert(New, -1, false); } } Invalidate(); } -void AddressList::OnSave(LDataStoreI *store, GDataIt l) +void AddressList::OnSave(LDataStoreI *store, LDataIt l) { l->DeleteObjects(); List a; GetAll(a); for (auto i: a) { LDataPropI *n = l->Create(store); if (n) { n->CopyProps(*i); l->Insert(n); } } } void AddressList::OnItemClick(LListItem *Item, LMouse &m) { LList::OnItemClick(Item, m); // Do the right click menu if (!Item && m.Right()) { LSubMenu *RClick = new LSubMenu; if (RClick) { RClick->AppendItem(LLoadString(IDS_ADD), IDM_NEW_CONTACT, true); if (GetMouse(m, true)) { switch (RClick->Float(this, m.x, m.y)) { case IDM_NEW_CONTACT: { LViewI *v = GetWindow()->FindControl(IDC_ADD); if (v) { LNotification note(LNotifyValueChanged); GetWindow()->OnNotify(v, note); } break; } } } DeleteObj(RClick); } } } void AddressList::Copy() { List Sel; if (GetSelection(Sel)) { LStringPipe p; for (auto i: Sel) { ListAddr *La = dynamic_cast(i); if (La) { char *s = La->Copy(); if (s) { p.Print("%s\r\n", s); DeleteArray(s); } } } char *t = p.NewStr(); if (t) { LClipBoard Clip(this); Clip.Text(t); char16 *w = Utf8ToWide(t); if (w) { Clip.TextW(w, false); DeleteArray(w); } DeleteArray(t); } } } void AddressList::Paste() { LClipBoard Clip(this); char *Txt = Clip.Text(); if (Txt) { LToken t(Txt, "\r\n"); for (unsigned i=0; iPaste(t[i]); Insert(La); } } } } bool AddressList::OnKey(LKey &k) { bool Status = false; if (k.Down()) { switch (k.vkey) { case LK_DELETE: { if (!k.IsChar) { List Sel; LList::GetSelection(Sel); for (auto i: Sel) { Delete(i); } Status = true; } break; } default: { switch (k.c16) { case 'c': case 'C': { Copy(); Status = true; break; } case 'v': case 'V': { Paste(); Status = true; break; } } break; } } } return LList::OnKey(k) || Status; } //////////////////////////////////////////////////////////////////////////// int AddrBrItemCmp(BrowseItem **a, BrowseItem **b) { if ((*a)->Score != (*b)->Score) { return (*b)->Score - (*a)->Score; } else { char *A = (*a)->First, *B = (*b)->First; if (A && B) { return _stricmp(A, B); } } return 0; } class AddressBrowsePluginResults : public LDom, public LMutex { LArray Results; LAutoString Msg; public: bool Dirty; AddressBrowsePluginResults() : LMutex("AddressBrowsePluginResults") { Dirty = false; } bool CallMethod(const char *MethodName, LVariant *ReturnValue, LArray &Args) { for (unsigned i=0; iType == GV_DOM) { Results.Add(v->Value.Dom); Dirty = true; } else if (v->Type == GV_STRING) { Msg.Reset(v->ReleaseStr()); Dirty = true; } } return true; } } PluginResults; class AddressBrowsePrivate { public: AddressBrowse *Ad; ScribeWnd *App; LEdit *Target; LList *Recip; LViewI *SetTo; LArray Items; LHashTbl, BrowseItem*> Has; ssize_t WordStart, WordEnd; AddressBrowsePrivate(AddressBrowse *ad) { Ad = ad; App = NULL; Target = NULL; Recip = NULL; SetTo = NULL; Recip = NULL; WordStart = WordEnd = 0; } void Test(LArray &Txt, const char *First, const char *Last, LString::Array &Email, const char *Nick) { if (Txt.Length() < 1) return; LAutoString TxtEmail; int i, EmailIdx = -1; for (i=0; i<(int)Txt.Length(); i++) { if (strchr(Txt[i], '@')) { TxtEmail.Reset(TrimStr(Txt[i], "{}[]<>()")); EmailIdx = i; break; } } #define ScoreText(Str, Search, Complete, First, Nth) \ if (Str) { char *s = Search; \ if (s) { \ size_t Len = strlen(s); \ if (_stricmp(Str, s) == 0) Score += Complete; \ if (_strnicmp(Str, s, Len) == 0) Score += First; \ else if (stristr(Str, s)) Score += Nth; \ } } for (i=0; i<(int)Email.Length(); i++) { char *Match = 0; int Score = 0; if (Txt.Length() == 1) { if (EmailIdx != 0) { ScoreText(First, Txt[0], 6, 5, 4); ScoreText(Last, Txt[0], 5, 4, 3); } } else if (Txt.Length() == 2) { if (EmailIdx != 0) ScoreText(First, Txt[0], 6, 5, 4); if (EmailIdx != 1) ScoreText(Last, Txt[1], 6, 5, 4); } // Full email match? if (TxtEmail) { if (stristr(Email[i], TxtEmail)) { Match = Email[i]; Score += 7; } } else { // Partial email match? if (stristr(Email[i], Txt[0])) { Match = Email[i]; Score += 2; } } if (TxtEmail && !Match) { // if they specify an email address and it not // found at all then this can't be the contact // they are looking for return; } Score += stristr(Nick, Txt[0]) ? 1 : 0; if (Score) { char *Addr = Match ? Match : Email[i].Get(); BrowseItem *i; if (!Has.Find(Addr)) { Items.Add(i = new BrowseItem(First, Last, Addr, Score)); if (i) { Has.Add(Addr, i); } } } } } void Update() { Items.DeleteObjects(); Has.Empty(); char *Txt = 0; char *RawTxt = (char*)Target->Name(); if (RawTxt) { ssize_t CharPos = Target->GetCaret(); ssize_t BytePos = LSeekUtf8(RawTxt, CharPos) - RawTxt; Txt = RawTxt + BytePos; if (Txt) { char *s = Txt; // Seek to the start of the name while (Txt > RawTxt && !strchr(MailAddressDelimiters, Txt[-1])) { Txt = LSeekUtf8(Txt, -1, RawTxt); } WordStart = Txt - RawTxt; // Seek to the end of the name Txt = s; while (Txt[0] && !strchr(MailAddressDelimiters, Txt[0])) { Txt = LSeekUtf8(Txt, 1); } WordEnd = Txt - RawTxt; // Store the sub-string Txt = NewStr(RawTxt + WordStart, WordEnd - WordStart); } } // printf("%s:%i - onupdate '%s'\n", _FL, Txt); if (ValidStr(Txt)) { Scan(Txt); // Put in the UI if (Ad) { // printf("Items=%i\n", (int)Items.Length()); Ad->SetItems(Items); } DeleteArray(Txt); } } bool Scan(LString Txt) { if (!App) return false; LToken t(Txt, " "); LHashTbl,Contact*> Contacts; App->HashContacts(Contacts); for (auto c : Contacts) { const char *First = 0, *Last = 0, *Nick = 0; c.value->Get(OPT_First, First); c.value->Get(OPT_Last, Last); c.value->Get(OPT_Nick, Nick); auto Emails = c.value->GetEmails(); Test(t, First, Last, Emails, Nick); } auto GrpSrcs = App->GetThingSources(MAGIC_GROUP); for (auto Groups: GrpSrcs) { if (!Groups->IsLoaded()) Groups->LoadThings(); // FIXME... this should refresh the browse list after load for (auto t : Groups->Items) { ContactGroup *Grp = t->IsGroup(); if (Grp) { LVariant Name; LArray Empty; if (Grp->GetVariant("Name", Name) && Name.Str()) { if (stristr(Name.Str(), Txt) && !Has.Find(Name.Str())) { BrowseItem *i; Items.Add(i = new BrowseItem(Name.Str(), 0, Name.Str(), 1)); if (i) { Has.Add(Name.Str(), i); } } } } } } // Sort Items.Sort(AddrBrItemCmp); return true; } }; bool AddressBrowseLookup(ScribeWnd *App, LArray &Items, LString s) { AddressBrowsePrivate d(NULL); d.App = App; if (!d.Scan(s)) return false; d.Items.Swap(Items); return true; } AddressBrowse::AddressBrowse(ScribeWnd *app, LView *target, LList *recip, LViewI *setto) : LPopupList(target, PopupBelow, target->X(), 150) { d = new AddressBrowsePrivate(this); d->App = app; d->Recip = recip; d->SetTo = setto; d->Target = dynamic_cast(target); Name("AddressBrowse"); } AddressBrowse::~AddressBrowse() { DeleteObj(d); } LString AddressBrowse::ToString(BrowseItem *Obj) { LString s; s.Printf("%s %s <%s>", Obj->First ? Obj->First.Get() : Empty, Obj->Last ? Obj->Last.Get() : Empty, Obj->Email.Get()); return s; } void AddressBrowse::OnSelect(BrowseItem *a) { if (a && a->Email) { LStringPipe p; const char *Cur = d->Target->Name(); if (Cur) { p.Push(Cur, d->WordStart); p.Push(Cur + d->WordEnd); } char *New = p.NewStr(); d->Target->Name(New); d->Target->SetCaret(d->WordStart); if (d->Recip) { char FullName[300], *Name = 0; if (a->First && a->Last) sprintf_s(Name = FullName, sizeof(FullName), "%s %s", a->First.Get(), a->Last.Get()); else if (a->First) Name = a->First; else if (a->Last) Name = a->Last; if (Name || a->Email) { ListAddr *n = new ListAddr(d->App, a->Email, Name); if (n) { if (d->SetTo) n->CC = (EmailAddressType) d->SetTo->Value(); d->Recip->Insert(n); } } } Visible(false); DeleteArray(New); } } int AddressBrowse::OnNotify(LViewI *c, LNotification n) { if (c == d->Target && n.Type == LNotifyValueChanged) { d->Update(); } else { // printf("%s:%i - no update %i %i\n", _FL, c == d->Target, !f); } return LPopupList::OnNotify(c, n); } diff --git a/Code/Encryption/GnuPG.cpp b/Code/Encryption/GnuPG.cpp --- a/Code/Encryption/GnuPG.cpp +++ b/Code/Encryption/GnuPG.cpp @@ -1,1970 +1,1970 @@ /* https://lists.gnupg.org/pipermail/gnupg-users/2011-November/043223.html gpg --list-packets ? Mime hierarchy: Signed: multipart/signed "This is an OpenPGP/MIME signed message (RFC 4880 and 3156)" { multipart/mixed text/plain Body of the message } application/pgp-signature "-----BEGIN PGP SIGNATURE-----" Signed, key attached: multipart/signed "This is an OpenPGP/MIME signed message (RFC 4880 and 3156)" { multipart/mixed text/plain Body of the message application/pgp-keys "-----BEGIN PGP PUBLIC KEY BLOCK-----" } application/pgp-signature "-----BEGIN PGP SIGNATURE-----" Signed, HTML, attachment, key: multipart/signed { multipart/mixed multipart/mixed multipart/alternative text/plain text/html application/octet-stream application/pgp-keys } application/pgp-signature Encrypted: multipart/encrypted application/pgp-encrypted application/octet-stream { multipart/mixed multipart/mixed text/plain } Encrypted + key attached: multipart/encrypted application/pgp-encrypted application/octet-stream { multipart/mixed multipart/mixed text/plain application/pgp-keys } */ #include "lgi/common/Lgi.h" #include "Scribe.h" #include "GnuPG.h" #include "lgi/common/TextLabel.h" #include "lgi/common/Button.h" #include "lgi/common/CheckBox.h" #include "lgi/common/Combo.h" #include "lgi/common/Css.h" #include "lgi/common/ThreadEvent.h" #include "lgi/common/SubProcess.h" #include "ScribeListAddr.h" #include "Store3Common.h" #include "lgi/common/LgiRes.h" #include "resdefs.h" #include "lgi/common/CssTools.h" ////////////////////////////////////////////////////////////////////////////////////////// enum Ctrls { IDC_SIGN = 700, IDC_ENCRYPT, IDC_ATTACH_PUB_KEY, IDC_DECRYPT, IDC_INSTALL }; static LColour cGood (0, 204, 0); static LColour cWarn (255, 154, 0); static LColour cError (255, 0, 0); static LColour cTxt (L_TEXT); #define cDefaultListItemColour 0 #define PANEL_BORDER_PX 2 static const char *GpgInstall = "https://www.gnupg.org/download/index.en.html"; static const char *GpgBin = "gpg" LGI_EXECUTABLE_EXT; static LString GpgBinPath; #define SECONDS * 1000 #define MINUTES * 60 #define GPG_KEY_STALE_TIMEOUT (10 MINUTES) #define DecryptStatus(val) \ if (callback) callback(val); \ return; typedef LArray KeyArr; typedef LAutoPtr KeyArrAuto; struct GpgJob { enum JobType { JobGetKeys, JobCheckSig, JobDecrypt, } Type; LViewI *Owner; GpgJob(JobType t) { Type = t; Owner = NULL; } // Get keys: LAutoPtr Emails; // Check sig: LAutoStreamI Msg; LMessage::Param UserValue; // Decrypt LString Password; }; struct LTempFile : public LFile { LString Path; LTempFile(const char *path) { if (Open(path, O_READ)) { Path = path; } } ~LTempFile() { if (Path) { Close(); FileDev->Delete(Path, false); } } }; struct GpgConnectorPriv : public LThread, public LMutex { LThreadEvent Event; private: bool Loop; KeyArr Keys; LArray Work; uint64 KeysTs; public: GpgConnectorPriv() : LThread("GpgConnectorPrivThread"), LMutex("GpgConnectorPrivMutex") { Loop = true; KeysTs = 0; Run(); } ~GpgConnectorPriv() { Loop = false; Event.Signal(); while (!IsExited()) LSleep(1); } void AddWork(GpgJob *j) { if (Lock(_FL)) { Work.Add(j); Unlock(); } } private: LString RunGpg(const char *Args) { LString Output; LSubProcess p(GpgBinPath, Args); if (p.Start(true, false)) { char Buf[256]; while (true) { ssize_t r = p.Read(&Buf, sizeof(Buf)-1); if (r > 0) { Buf[r] = 0; Output += Buf; } else break; } } return Output; } void ParseKeys(KeyArr &k, LString &str) { bool GotDash = false; LString::Array a = str.Split(EOL_SEQUENCE); LString KeyId; for (unsigned i=0; i 0) { if (b[0].Equals("sec") || b[0].Equals("pub")) { if (b.Length() == 4) KeyId = b[2]; } else if (b[0].Equals("uid")) { LString Nm = Ln(21, -1).Strip(); LAutoString Name, Addr; DecodeAddrName(Nm, Name, Addr, NULL); if (ValidStr(Name) && ValidStr(Addr)) { GpgConnector::KeyInfo &Cur = k.New(); Cur.KeyId = KeyId; Cur.Name = Name; Cur.Email = Addr; } else LgiTrace("%s:%i - Error parsing '%s'\n", _FL, Nm.Get()); } } } else if (stristr(Ln, "--------")) { GotDash = true; } } } bool GetKeys() { uint64 Now = LCurrentTime(); if (Now - KeysTs > GPG_KEY_STALE_TIMEOUT) { KeysTs = Now; LString s = RunGpg("-k"); if (s) ParseKeys(Keys, s); s = RunGpg("-K"); if (s) { KeyArr Priv; ParseKeys(Priv, s); LHashTbl, GpgConnector::KeyInfo*> Hash; for (auto &k: Keys) { if (k.Email) Hash.Add(k.Email, &k); else LgiTrace("%s:%i - No email for key?\n", _FL); } for (unsigned i=0; iFlags |= GPG_HAS_PRIV_KEY; } } } return Keys.Length() > 0; } bool Save(const char *Path, LString &s) { LFile f; if (!f.Open(Path, O_WRITE)) return false; f.SetSize(0); return f.Write(s, s.Length()) == s.Length(); } bool Save(const char *Path, LStreamI *s) { LFile f; if (!f.Open(Path, O_WRITE)) return false; f.SetSize(0); LCopyStreamer Cp; int64 SrcSz = s->GetSize(); int64 Copied = Cp.Copy(s, &f); return SrcSz == Copied; } void CheckSignature(LViewI *Owner, LAutoStreamI UserMsg, LMessage::Param UserVal) { LString Msg; int64 Sz = 0; int64 Rd = 0; ptrdiff_t HdrSize = 0; LString Start; LArray Segs; LMime Tmp; LString Hdrs; char *Boundary = NULL; LAutoPtr Resp(new GpgSigCheckResponse); if (!Resp) return; // Not much else we can do. Resp->UserValue = UserVal; if (!UserMsg) { Resp->Error.Printf(LLoadString(IDS_GNUPG_ERR_NOMSG)); goto OnSigCheckError; } // We have to parse out just the part of the email that is signed. And we // can't use the LMime class because that would decode it too much and lose // the exact formatting. Read the whole thing into memory and find the MIME // boundary. Sz = UserMsg->GetSize(); UserMsg->SetPos(0); if (!Msg.Length((int)Sz)) { Resp->Error.Printf("Can't size string to " LPrintfInt64, Sz); goto OnSigCheckError; } for (int64 i=0; iRead(Msg.Get() + i, Msg.Length() - i); if (Rd <= 0) { Resp->Error.Printf("Read error: i=%i, Rd=%i, Sz=" LPrintfInt64, i, Rd, Sz); goto OnSigCheckError; } i += Rd; } HdrSize = Msg.Find("\r\n\r\n"); Hdrs = Msg(0, HdrSize); Tmp.SetHeaders(Hdrs); Boundary = Tmp.GetBoundary(); if (!Boundary) { Resp->Error = LLoadString(IDS_GNUPG_ERR_NO_BOUNDARY); goto OnSigCheckError; } // Now look through the message and find all the segments... Start.Printf("\r\n--%s", Boundary); for (ptrdiff_t i = 0; i < (ptrdiff_t)Msg.Length(); ) { ptrdiff_t Next = Msg.Find(Start, i); if (Next > 0) { // Is it an end or starting boundary? ptrdiff_t k = Next + Start.Length(); LString Chars = Msg(k, k + 2); bool IsEnd = Chars == "--"; if (IsEnd) { if (Segs.Length() == 0) { Resp->Error = LLoadString(IDS_GNUPG_ERR_NO_MIME); goto OnSigCheckError; } Segs.Last().Len = Next - Segs.Last().Start; i = k + 2; } else { if (Segs.Length() > 0) { // Finish last segment Segs.Last().Len = Next - Segs.Last().Start; } char *m = Msg; while (IsWhiteSpace(m[k])) k++; LRange &r = Segs.New(); r.Start = k; r.Len = 0; i = k; } } else break; } if (Segs.Length() == 2) { // char *m = Msg; LRange &Body = Segs[0]; LRange &Sig = Segs[1]; // Get the 2 parts from the whole message.. LString SignedText = Msg(Body.Start, Body.Start + Body.Len); LString Signature = Msg(Sig.Start, Sig.Start + Sig.Len); ptrdiff_t Break = Signature.Find("\r\n\r\n"); if (Break > 0) Signature = Signature(Break + 4, -1); // Save them to files: LFile::Path TextPath = ScribeTempPath(), SigPath = ScribeTempPath(); TextPath += "signed.txt"; SigPath += "signature.txt"; if (!Save(TextPath, SignedText) || !Save(SigPath, Signature)) { Resp->Error = LLoadString(IDS_GNUPG_ERR_SAVE_FAILED); goto OnSigCheckError; } // Now call GnuPG to verify the signature... LString Args; Args.Printf("--verify \"%s\" \"%s\"", SigPath.GetFull().Get(), TextPath.GetFull().Get()); LString Output = RunGpg(Args); if (Output) { LString::Array a = Output.SplitDelimit("\n"); for (unsigned i=0; i= 0) { LString::Array w = Line.SplitDelimit(" "); // Check if the signature is good... if (w.Length() > 2) Resp->SignatureMatch = w[1].Lower() == "good"; // Get identity... w = Line.SplitDelimit("\""); if (w.Length() >= 2 && w[1].Find("@") >= 0) { Resp->Identity = w[1]; } } else if (Line.Lower().Find("signature made") >= 0) { // Get date... LString::Array w = Line.SplitDelimit(" "); int MadeIdx = -1; LString::Array DateParts; for (unsigned i=0; i 0) Resp->TimeStamp.SetDate(w[i]); else if (w[i].Find(":") > 0) { Resp->TimeStamp.SetTime(w[i]); MadeIdx = -1; } else if (w[i].Lower() == "made") MadeIdx = i; else if (MadeIdx >= 0 && (int)i > MadeIdx) DateParts.New() = w[i]; } if (Resp->TimeStamp.Year() == 0 && DateParts.Length() > 0) { LString s = LString(" ").Join(DateParts); Resp->TimeStamp.SetDate(s); } } } } else { Resp->Error = LLoadString(IDS_GNUPG_ERR_NO_OUTPUT); } #ifndef _DEBUG // Clean up temporary files... FileDev->Delete(TextPath, false); FileDev->Delete(SigPath, false); #endif printf("Txt=%s\nSig=%s\n", (const char*)TextPath, (const char*)SigPath); } else { Resp->Error.Printf(LLoadString(IDS_GNUPG_ERR_WRONG_SEGS), Segs.Length()); } OnSigCheckError: Owner->PostEvent(M_GNUPG_SIG_CHECK, (LMessage::Param) Resp.Release()); } void Decrypt(GpgJob *j) { LAutoPtr Resp(new GpgDecryptResponse); if (!Resp || !j) return; Resp->UserValue = j->UserValue; LFile::Path InPath = ScribeTempPath(); InPath += "encrypted.txt"; LFile::Path OutPath = ScribeTempPath(); OutPath += "decrypted.txt"; if (!Save(InPath, j->Msg)) { Resp->Error = "Failed to save file for decrypting."; } else { LString Args; Args.Printf("--batch --passphrase-fd 0 --output \"%s\" --decrypt \"%s\"", OutPath.GetFull().Get(), InPath.GetFull().Get()); LSubProcess Proc(GpgBinPath, Args); if (!Proc.Start(true, true)) { Resp->Error = "Can't start the GnuPG sub-process."; } else { char Buf[256]; LVariant v; if (Proc.GetValue(LDomPropToString(StreamReadable), v) && v.CastInt32() != 0) { ssize_t r = Proc.Read(Buf, sizeof(Buf)-1); Buf[MAX(r, 0)] = 0; } LString PswStr; PswStr.Printf("%s\n", j->Password.Get()); ssize_t w = Proc.Write(PswStr.Get(), PswStr.Length()); if (w < 0) { ssize_t r = Proc.Read(Buf, sizeof(Buf)-1); Buf[MAX(r, 0)] = 0; Resp->Error = Buf[0] ? Buf : "Can't write to the GnuPG sub-process."; } else { ssize_t r = Proc.Read(Buf, sizeof(Buf)-1); Buf[MAX(r, 0)] = 0; int Result = Proc.Wait(); LFile *f; if (Result) { if (Buf[0]) Resp->Error = Buf[0]; else Resp->Error.Printf("GnuPG encryption process failed with code: %i", Result); } else if ( !Resp->Data.Reset(f = new LTempFile(OutPath)) || !f->IsOpen()) { Resp->Data.Reset(); Resp->Error.Printf("Decrypt failed: Can't open '%s' for reading.", OutPath.GetFull().Get()); } else { // Success? } } } } // Clean up input file (encrypted) // The output file will be deleted by the M_GNUPG_DECRYPT handler. if (LFileExists(InPath)) { FileDev->Delete(InPath, false); } j->Owner->PostEvent(M_GNUPG_DECRYPT, (LMessage::Param) Resp.Release()); } int Main() { LThreadEvent::WaitStatus s; while ((s = Event.Wait()) == LThreadEvent::WaitSignaled) { if (!Loop) break; LAutoPtr j; if (Lock(_FL)) { if (Work.Length()) { j.Reset(Work[0]); Work.DeleteAt(0, true); } Unlock(); } if (j) { switch (j->Type) { case GpgJob::JobGetKeys: { if (Keys.Length() == 0) GetKeys(); LHashTbl,bool> Map; for (unsigned i=0; iEmails->Length(); i++) { Map.Add((*j->Emails)[i], true); } KeyArrAuto Inf(new KeyArr); for (unsigned i=0; iNew(); out.Email = in.Email.Get(); out.Name = in.Name.Get(); out.KeyId = in.KeyId.Get(); out.Flags = in.Flags; } j->Owner->PostEvent(M_GNUPG_KEY_INFO, 0, (LMessage::Param)Inf.Release()); break; } case GpgJob::JobCheckSig: { CheckSignature(j->Owner, j->Msg, j->UserValue); break; } case GpgJob::JobDecrypt: { Decrypt(j); break; } default: { LAssert(!"Invalid type."); break; } } } } return 0; } }; bool GpgConnector::IsInstalled() { #ifdef WINNATIVE char *Str = NULL; errno_t Err = _dupenv_s(&Str, NULL, "PATH"); if (Err) { LgiTrace("%s:%i - _dupenv_s failed with %i\n", _FL, Err); return false; } LString Path = Str; free(Str); #else LString Path = getenv("PATH"); #ifdef MAC Path += LGI_PATH_SEPARATOR"/opt/local/bin"; #endif #endif LString::Array Parts = Path.Split(LGI_PATH_SEPARATOR); for (unsigned i = 0; i < Parts.Length(); i++) { LFile::Path p(Parts[i]); p += GpgBin; if (p.IsFile()) { GpgBinPath = p; return true; } } return false; } GpgConnector::GpgConnector() { d = new GpgConnectorPriv; } GpgConnector::~GpgConnector() { delete d; } bool GpgConnector::GetKeyInfo(LViewI *Target, LString::Array &Emails) { if (!Target #if LGI_VIEW_HANDLE || !Target->Handle() #endif ) { LAssert(!"Invalid target."); return false; } if (!d->Lock(_FL)) return false; GpgJob *j = new GpgJob(GpgJob::JobGetKeys); if (j) { j->Emails.Reset(new LString::Array(Emails)); j->Owner = Target; d->AddWork(j); } d->Unlock(); return d->Event.Signal(); } bool GpgConnector::CheckSignature(LViewI *Target, LAutoStreamI Rfc822Msg, LMessage::Param UserVal) { if (!Target #if LGI_VIEW_HANDLE || !Target->Handle() #endif ) { LAssert(!"Invalid target."); return false; } if (!d->Lock(_FL)) return false; GpgJob *j = new GpgJob(GpgJob::JobCheckSig); if (j) { j->Owner = Target; j->Msg = Rfc822Msg; j->UserValue = UserVal; d->AddWork(j); } d->Unlock(); return d->Event.Signal(); } bool GpgConnector::Decrypt(LViewI *Target, LAutoStreamI Data, LString Password, LMessage::Param UserVal) { if (!Target #if LGI_VIEW_HANDLE || !Target->Handle() #endif ) { LAssert(!"Invalid target."); return false; } if (!d->Lock(_FL)) return false; GpgJob *j = new GpgJob(GpgJob::JobDecrypt); if (j) { j->Owner = Target; j->Msg = Data; j->Password = Password; j->UserValue = UserVal; d->AddWork(j); } d->Unlock(); return d->Event.Signal(); } //////////////////////////////////////////////////////////////////////////////////////////////// struct MailUiGpgPriv { struct UserPassword { uint64 Ts; LString Email; LString Password; }; // Objs ScribeWnd *App; MailUi *Ui; // UI LCheckBox *Enc; LCheckBox *Sign; LCheckBox *Attach; LTextLabel *Msg; KeyArrAuto Inf; LTableLayout *Table; LButton *Decrypt, *Install; // Options bool WritingEmail; bool Encrypted; bool Signed; // Passwords LArray Psw; MailUiGpgPriv(ScribeWnd *app, MailUi *ui, bool writingEmail) { App = app; Ui = ui; Enc = NULL; Sign = NULL; Attach = NULL; Msg = NULL; Table = NULL; Decrypt = NULL; Install = NULL; WritingEmail = writingEmail; Encrypted = false; Signed = false; } AddressList *GetAddrLst() { AddressList *AddrLst = NULL; Ui->GetViewById(IDC_TO, AddrLst); return AddrLst; } void NotInstalled() { SetError(LLoadString(IDS_GNUPG_ERR_NOT_INSTALLED)); if (Install) Install->Visible(true); if (Enc) Enc->Enabled(false); if (Sign) Sign->Enabled(false); if (Attach) Attach->Enabled(false); } void Set(const char *Str, LColour &Col) { if (!Msg || !Table) { LgiTrace("%s:%i - Can't set message.\n", _FL); return; } Msg->GetCss(true)->Color(Col); Msg->Name(Str); LNotification note(LNotifyTableLayoutRefresh); Table->OnNotify(Msg, note); } void SetError(const char *Str) { Set(Str, cError); } void SetWarning(const char *Str) { Set(Str, cWarn); } void SetStatus(const char *Str) { Set(Str, cTxt); } void SetSuccess(const char *Str) { Set(Str, cGood); } void GetPassword(LViewI *Parent, LString Addr, std::function Callback) { for (unsigned i=0; iDoModal([this, Dlg, Addr, Callback](auto dlg, auto ctrlId) { if (ctrlId) { UserPassword &p = Psw.New(); p.Email = Addr; p.Password = Dlg->GetStr(); p.Ts = LCurrentTime(); if (Callback) Callback(p.Password); } delete dlg; }); } }; LCss::Len Px(int px) { return LCss::Len(LCss::LenPx, (float)px); } MailUiGpg::MailUiGpg(ScribeWnd *App, MailUi *Ui, int ColX1, int ColX2, bool WritingEmail) { d = new MailUiGpgPriv(App, Ui, WritingEmail); int TextY = 6; #ifdef MAC int ChkY = 1; #else int ChkY = 6; #endif if (AddView(d->Table = new LTableLayout(50))) { Mail *m = Ui->GetItem(); LDataI *obj = m ? m->GetObject() : NULL; int x = 0; LTextLabel *Txt; auto *c = d->Table->GetCell(x++, 0); c->Position(LCss::PosAbsolute); c->Left(Px(ColX1 - PANEL_BORDER_PX)); c->Top(Px(TextY - PANEL_BORDER_PX)); c->Add(Txt = new LTextLabel(IDC_STATIC, ColX1, TextY, -1, -1, "GnuPG:")); if (WritingEmail) { c = d->Table->GetCell(x++, 0); c->Position(LCss::PosAbsolute); c->Left(Px(ColX2 - PANEL_BORDER_PX + 1)); c->Top(Px(ChkY - PANEL_BORDER_PX)); c->Add(d->Enc = new LCheckBox(IDC_ENCRYPT, 0, 0, -1, -1, LLoadString(IDS_GNUPG_ENCRYPT))); c = d->Table->GetCell(x++, 0); c->PaddingTop(Px(ChkY - PANEL_BORDER_PX)); c->Add(d->Sign = new LCheckBox(IDC_SIGN, 0, 0, -1, -1, LLoadString(IDS_GNUPG_SIGN))); c = d->Table->GetCell(x++, 0); c->PaddingTop(Px(ChkY - PANEL_BORDER_PX)); c->Add(d->Attach = new LCheckBox(IDC_ATTACH_PUB_KEY, 0, 0, -1, -1, LLoadString(IDS_GNUPG_ATTACH_PUB_KEY))); } else { c = d->Table->GetCell(x++, 0); c->Position(LCss::PosAbsolute); c->Left(Px(ColX2 - PANEL_BORDER_PX + 1)); if (c->Add(d->Decrypt = new LButton(IDC_DECRYPT, 0, 0, -1, -1, LLoadString(IDS_GNUPG_DECRYPT)))) { LDataPropI *seg = obj ? obj->GetObj(FIELD_MIME_SEG) : NULL; auto mimeType = seg ? seg->GetStr(FIELD_MIME_TYPE) : NULL; if (mimeType) { d->Signed = !_stricmp(mimeType, sMultipartSigned); d->Encrypted = !_stricmp(mimeType, sMultipartEncrypted); } d->Decrypt->Enabled(d->Encrypted); } } c = d->Table->GetCell(x++, 0); c->PaddingTop(Px(TextY - PANEL_BORDER_PX)); c->PaddingLeft(LCss::Len(LCss::LenEm, 1.0f)); c->Add(d->Msg = new LTextLabel(IDC_MSG, 0, 0, 300, -1, "...")); c = d->Table->GetCell(x++, 0); c->TextAlign(LCss::AlignRight); if ((d->Install = new LButton(IDC_INSTALL, 0, 0, -1, -1, LLoadString(IDS_GNUPG_GET_GPG)))) { d->Install->Visible(false); c->Add(d->Install); } if (d->Encrypted) { d->SetWarning("Message is encrypted."); } } } MailUiGpg::~MailUiGpg() { delete d; if (GetParent()) { LNotification note(LNotifyItemDelete); GetParent()->OnNotify(this, note); } } void MailUiGpg::OnRecipientChange() { if (!d->WritingEmail || !d->Enc || !d->Sign) return; // Don't care... AddressList *AddrLst = d->GetAddrLst(); if (!AddrLst) { d->SetError("No AddressList."); return; } GpgConnector *Gpg = d->App->GetGpgConnector(); if (!Gpg) { d->NotInstalled(); return; } List a; if (!AddrLst->GetAll(a)) { d->SetStatus(LLoadString(IDS_GNUPG_ERR_NO_RECIP)); return; } if (d->Enc->Value() || d->Sign->Value()) { // Do we have public keys for each of the recipients? d->SetWarning(LLoadString(IDS_GNUPG_CHECKING)); LString::Array Emails; for (auto i: a) { if (i->sAddr) Emails.New() = i->sAddr; } LCombo *cbo; if (d->Ui->GetViewById(IDC_FROM, cbo)) { const char *Frm = cbo->Name(); LAutoString Name, Email; DecodeAddrName(Frm, Name, Email, NULL); if (Email) Emails.New() = Email; } Gpg->GetKeyInfo(this, Emails); } else { d->SetStatus(LLoadString(IDS_GNUPG_ERR_NO_SIGN_ENC)); // Revert list items to no colour... for (auto i: a) { i->SetInt(FIELD_COLOUR, cDefaultListItemColour); i->Update(); } } } // This gets all the events from MailUi::OnNotify as well as any events // from MailUiGpg's child controls. // \returns non-zero if further processing in MailUi::OnNotify should be blocked. int MailUiGpg::OnNotify(LViewI *Ctrl, LNotification n) { switch (Ctrl->GetId()) { case IDC_TO: { switch (n.Type) { case LNotifyItemInsert: case LNotifyItemDelete: // Recipients changed... check for keys... OnRecipientChange(); break; default: break; } break; } case IDC_ENCRYPT: case IDC_SIGN: { OnRecipientChange(); break; } case IDC_DECRYPT: { Decrypt(NULL); break; } case IDC_INSTALL: { LExecute(GpgInstall); break; } } return 0; } void DeleteChildSegments(LDataPropI *d) { - GDataIt It = d->GetList(FIELD_MIME_SEG); + LDataIt It = d->GetList(FIELD_MIME_SEG); for (unsigned i=0; iLength(); ) { LDataPropI *c = (*It)[i]; DeleteChildSegments(c); LDataI *cdi = dynamic_cast(c); if (cdi) { // This deletes the on disk representation. cdi->Delete(); // This removes the object from the segment tree and // frees the memory delete cdi; } else { LgiTrace("%s:%i - DeleteChildSegments: Wrong object type.\n", _FL); LAssert(0); i++; // Skip this? Maybe we should break? } } } LString MailUiGpg::GetPublicKey(const char *Email) { LString s; if (Email) { LString Args; Args.Printf("-a --export %s", Email); LSubProcess Proc(GpgBinPath, Args); if (!Proc.Start(true, false)) { d->SetError(LLoadString(IDS_GNUPG_ERR_CANT_START)); return 1; } ssize_t r; char Buf[256] = ""; while ((r = Proc.Read(Buf, sizeof(Buf)-1)) > 0) { Buf[r] = 0; s += Buf; } int Result = Proc.Wait(); if (Result) { LString Msg; Msg.Printf(LLoadString(IDS_GNUPG_ERR_CODE), Result); d->SetError(Buf[0] ? Buf : Msg); s.Empty(); } } return s; } bool MailUiGpg::ReadFile(LArray &Data, const char *Path) { LFile f; if (!f.Open(Path, O_READ)) { LString s; s.Printf(LLoadString(IDS_ERROR_CANT_READ), Path); d->SetError(s); return false; } if (f.GetSize() < 0) { LString s; s.Printf(LLoadString(IDS_GNUPG_ERR_NO_CONTENT), Path); d->SetError(s); return false; } if (!Data.Length((uint32_t)f.GetSize())) { d->SetError("Memory alloc failed."); return 1; } if (f.Read(&Data[0], Data.Length()) != Data.Length()) { LString s; s.Printf(LLoadString(IDS_GNUPG_ERR_READ_FAIL), Path); d->SetError(s); return false; } return true; } void MailUiGpg::Decrypt(std::function callback) { // Get the connector... GpgConnector *Conn = d->App->GetGpgConnector(); if (!Conn) { d->NotInstalled(); DecryptStatus(1); } // Find the right attachment... Mail *m = d->Ui->GetItem(); if (!m) { d->SetError(LLoadString(IDS_GNUPG_ERR_NO_MAIL)); DecryptStatus(1); } LDataPropI *Root = m->GetObject()->GetObj(FIELD_MIME_SEG); if (!Root) { d->SetError(LLoadString(IDS_GNUPG_ERR_NO_ROOT)); DecryptStatus(1); } auto Mt = Root->GetStr(FIELD_MIME_TYPE); if (!Mt || _stricmp(Mt, sMultipartEncrypted)) { d->SetError(LLoadString(IDS_GNUPG_ERR_WRONG_MIME)); DecryptStatus(1); } LDataI *EncryptedObj = NULL; LArray Objs; if (!m->GetAttachmentObjs(Objs)) DecryptStatus(1); for (unsigned i=0; iGetStr(FIELD_MIME_TYPE); if (Mt && !_stricmp(Mt, sAppOctetStream)) { EncryptedObj = Objs[i]; break; } } if (!EncryptedObj) { d->SetError(LLoadString(IDS_GNUPG_ERR_NO_ENC_MIME)); DecryptStatus(1); } // Get the password for decryption... LHashTbl,ScribeAccount*> Map; for (auto a : *m->App->GetAccounts()) { LVariant v = a->Identity.Email(); if (v.Str()) { char *Email = v.Str(); if (Email) Map.Add(Email, a); } } - GDataIt ToLst = m->GetTo(); + LDataIt ToLst = m->GetTo(); LString::Array ToEmail; if (ToLst) { for (LDataPropI *t = ToLst->First(); t; t = ToLst->Next()) { auto Email = t->GetStr(FIELD_EMAIL); if (Email) { if (Map.Find(Email)) ToEmail.New() = Email; } } } if (ToEmail.Length() == 0) { d->SetError(LLoadString(IDS_GNUPG_ERR_NO_ID)); DecryptStatus(1); } d->GetPassword(d->Ui, ToEmail[0], [this, callback, EncryptedObj, Conn, m](auto Pass) { if (!Pass) { d->SetWarning(LLoadString(IDS_GNUPG_DECRYPT_CANCEL)); DecryptStatus(1); } // Send the file to by decrypted... LAutoStreamI Data = EncryptedObj->GetStream(_FL); if (!Data) { d->SetError(LLoadString(IDS_GNUPG_ERR_NO_DATA)); DecryptStatus(1); } if (!Conn->Decrypt(this, Data, Pass, (LMessage::Param) m)) { d->SetError(LLoadString(IDS_GNUPG_ERR_DECRYPT_FAIL)); DecryptStatus(1); } DecryptStatus(0); }); } void MailUiGpg::SignEncrypt(bool uSign, bool uEncrypt, bool uAttachPublicKey, std::function callback) { // Save the message normally Mail *m = d->Ui->GetItem(); bool IsInPublicFolder = m->GetFolder() && m->GetFolder()->IsPublicFolders(); if (IsInPublicFolder) { LDateTime n; n.SetNow(); m->SetDateSent(&n); m->Update(); } d->Ui->OnDataEntered(); d->Ui->OnSave(); LDataI *Root = dynamic_cast(m->GetObject()->GetObj(FIELD_MIME_SEG)); if (!Root) { d->SetError(LLoadString(IDS_GNUPG_ERR_NO_ROOT)); DecryptStatus(1); } // Check that the send has a private key setup... LString FromEmail = m->GetFromStr(FIELD_EMAIL); if (!FromEmail) { d->SetError(LLoadString(IDS_GNUPG_ERR_NO_SENDER_EMAIL)); DecryptStatus(1); } LString PrivKeyId; if (d->Inf) { for (unsigned i=0; iInf->Length(); i++) { GpgConnector::KeyInfo &ki = (*d->Inf)[i]; if (!_stricmp(ki.Email, FromEmail)) { PrivKeyId = ki.KeyId; break; } } } if (!PrivKeyId) { LString s; s.Printf(LLoadString(IDS_GNUPG_ERR_NO_PRIV_KEY), FromEmail.Get()); d->SetError(s); DecryptStatus(1); } if (uAttachPublicKey) { LString PubKey = GetPublicKey(FromEmail); if (!PubKey) { LString s; s.Printf(LLoadString(IDS_GNUPG_ERR_NO_PUB_KEY), FromEmail.Get()); d->SetError(s); DecryptStatus(1); } Attachment *a = new Attachment(m->App); if (a) { LAutoStreamI Data(new LMemStream(PubKey, PubKey.Length())); if (Data) { if (!a->ImportStream("public-key.asc", "application/pgp-keys", Data)) { d->SetError(LLoadString(IDS_GNUPG_ERR_IMPORT_FAIL)); DecryptStatus(1); } else { m->AttachFile(a); } } else { d->SetError("Allocation failed."); DecryptStatus(1); } } } // Re-write the MIME hierarchy to have the message and attachments encrypted // 1) Get the password d->GetPassword(d->Ui, FromEmail.Get(), [this, callback, InputRoot=Root, uSign, uEncrypt, m, PrivKeyId](auto Psw) { LDataI *LocalRoot = InputRoot; if (!Psw) { d->SetStatus(LLoadString(IDS_GNUPG_ERR_SIGN_ENC_CANCEL)); DecryptStatus(1); } // 1) Export the message to a file: const char *BaseName = "encrypted.asc"; LFile::Path p = ScribeTempPath(); p += BaseName; LFile f; if (!f.Open(p, O_READWRITE)) { d->SetError(LLoadString(IDS_GNUPG_ERR_TEMP_WRITE)); DecryptStatus(1); } f.SetSize(0); f.SetPos(0); LMime Mime(ScribeTempPath()); Store3ToGMime(&Mime, LocalRoot); if (!Mime.GetBoundary()) { // No boundary... so set it and propagate the change back char b[64]; CreateMimeBoundary(b, sizeof(b)); Mime.SetBoundary(b); LocalRoot->SetStr(FIELD_INTERNET_HEADER, Mime.GetHeaders()); // If we don't do this then LMime will create it again later // when we actually go to send the message, but it will be // different then and the signing will fail. } if (!Mime.Text.Encode.Push(&f)) { d->SetError(LLoadString(IDS_GNUPG_ERR_EXPORT_TEMP)); DecryptStatus(1); } #if 1 if (uSign) { // Remove any white space from the end of the file... this is // to make sure the signing process is standardized. See // https://www.ietf.org/rfc/rfc3156.txt // Part 5: OpenPGP signed data // This is probably not very efficient but it's usually only the // 2 bytes: "\r\n" int64 Size, Pos; while ( (Size = f.GetSize()) > 0) { Pos = f.SetPos(Size-1); if (Pos != Size - 1) break; char c; ssize_t Rd = f.Read(&c, 1); if (Rd == 1 && strchr(WhiteSpace, c)) { f.SetSize(Size - 1); } else break; } } #endif f.Close(); if (!uEncrypt) { // Just signing... move MIME tree into child node LDataI *NewRoot = LocalRoot->GetStore()->Create(MAGIC_ATTACHMENT); if (!NewRoot) { d->SetError(LLoadString(IDS_GNUPG_ERR_NEW_ATTACH_FAIL)); DecryptStatus(1); } // Copy over the root node headers NewRoot->SetStr(FIELD_INTERNET_HEADER, LocalRoot->GetStr(FIELD_INTERNET_HEADER)); // Reparent the old root to the new root, and then attach that to the message... if (!LocalRoot->Save(NewRoot) || !m->GetObject()->SetObj(FIELD_MIME_SEG, NewRoot)) { d->SetError(LLoadString(IDS_GNUPG_ERR_REPARENT)); DecryptStatus(1); } LocalRoot = NewRoot; } // 2) Encrypt/sign the file: LString InFile(p); p--; p += "encrypted.gpg"; LString OutFile(p); if (LFileExists(OutFile)) { FileDev->Delete(OutFile, false); } LString Args, s; Args.Printf("--batch --passphrase-fd 0 -u 0x%s", PrivKeyId.Get()); if (uEncrypt) { - GDataIt To = m->GetTo(); + LDataIt To = m->GetTo(); for (LDataPropI *Recip = To->First(); Recip; Recip = To->Next()) { auto Email = Recip->GetStr(FIELD_EMAIL); LAssert(Email != NULL); if (Email) { s.Printf(" --recipient %s", Email); Args += s; } else { d->SetError(LLoadString(IDS_GNUPG_ERR_RECIP_NO_EMAIL)); DecryptStatus(1); } } } s.Printf(" --armor -o \"%s\" %s \"%s\"", OutFile.Get(), uSign && uEncrypt ? "-se" : (uSign ? "--detach-sign" : "-e"), InFile.Get()); Args += s; LSubProcess Proc(GpgBinPath, Args); char Buf[256]; if (!Proc.Start(true, true)) { d->SetError(LLoadString(IDS_GNUPG_ERR_CANT_START)); DecryptStatus(1); } LString PswStr; PswStr.Printf("%s\n", Psw.Get()); ssize_t w = Proc.Write(PswStr.Get(), PswStr.Length()); if (w < 0) { d->SetError(LLoadString(IDS_GNUPG_ERR_WRITE)); DecryptStatus(1); } ssize_t r = Proc.Read(Buf, sizeof(Buf)-1); Buf[MAX(r, 0)] = 0; int Result = Proc.Wait(); if (Result) { d->SetError(Buf[0] ? Buf : LLoadString(IDS_GNUPG_ERR_ENCRYPT_FAIL)); DecryptStatus(1); } // 3) Import the encrypted message and replace contents of MIME tree. LArray InData, OutData; if (!ReadFile(OutData, OutFile)) DecryptStatus(1); #ifndef _DEBUG // Clean up temporary files... FileDev->Delete(InFile, false); FileDev->Delete(OutFile, false); #endif if (uEncrypt) { // Clear out all existing attachments... DeleteChildSegments(LocalRoot); } // Setup the root MIME node to have the right type and fields... LAutoStreamI Data; { // By using a LMime object we preserve the existing headers in the MIME // segment while still being able to change the MIME type and charset. LMime Tmp; Tmp.SetHeaders(LocalRoot->GetStr(FIELD_INTERNET_HEADER)); Tmp.SetMimeType(uSign ? sMultipartSigned : sMultipartEncrypted); Tmp.SetCharset("utf-8"); Tmp.SetSub( "Content-Type", "protocol", uEncrypt ? "application/pgp-encrypted" : "application/pgp-signature"); LocalRoot->SetStr(FIELD_INTERNET_HEADER, Tmp.GetHeaders()); // Set a body message const char *BodyMsg = "This is an OpenPGP/MIME encrypted message (RFC 4880 and 3156)\n"; Data.Reset(new LMemStream(BodyMsg, strlen(BodyMsg))); LocalRoot->SetStream(Data); LocalRoot->Save(); } if (uEncrypt) { // Attach some app info... LDataI *AppInfo = LocalRoot->GetStore()->Create(MAGIC_ATTACHMENT); if (!AppInfo) { d->SetError(LLoadString(IDS_GNUPG_ERR_NEW_ATTACH_FAIL)); DecryptStatus(1); } AppInfo->SetStr(FIELD_MIME_TYPE, "application/pgp-encrypted"); const char *AppInfoMsg = "Version: 1\n"; Data.Reset(new LMemStream(AppInfoMsg, strlen(AppInfoMsg))); AppInfo->SetStream(Data); AppInfo->Save(LocalRoot); } { // Attach the new data to the email... LDataI *File = LocalRoot->GetStore()->Create(MAGIC_ATTACHMENT); if (!File) { d->SetError(LLoadString(IDS_GNUPG_ERR_NEW_ATTACH_FAIL)); DecryptStatus(1); } if (uEncrypt) { // Attach the encrypted data here... LMime Tmp; Tmp.SetMimeType(sAppOctetStream); Tmp.SetFileName(BaseName); Tmp.Set("Content-Disposition", "inline"); Tmp.SetSub("Content-Disposition", "filename", BaseName); File->SetStr(FIELD_INTERNET_HEADER, Tmp.GetHeaders()); } else // uSign { // Set up the signature attachment LMime Tmp; Tmp.SetMimeType("application/pgp-signature"); Tmp.SetFileName(BaseName); Tmp.Set("Content-Description", "OpenPGP digital signature"); Tmp.Set("Content-Disposition", "attachment"); Tmp.SetSub("Content-Disposition", "filename", BaseName); File->SetStr(FIELD_INTERNET_HEADER, Tmp.GetHeaders()); } Data.Reset(new LMemStream(&OutData[0], OutData.Length())); File->SetStream(Data); File->Save(LocalRoot); } // Tell the UI that the object has changed... LArray ChangeArr; ChangeArr.Add(m->GetObject()); d->App->SetContext(_FL); d->App->OnChange(ChangeArr, 0); DecryptStatus(0); }); } void MailUiGpg::DoCommand(int Cmd, std::function callback) { switch (Cmd) { case IDM_SEND_MSG: { if (!d || !d->Enc || !d->Sign || !d->Attach || !d->Ui) { // Resending an old mail. // There is no Enc/Sign UI. DecryptStatus(0); } if (!d->Enc->Value() && !d->Sign->Value()) { DecryptStatus(0); // No need to sign &| encrypt } Mail *m = d->Ui->GetItem(); if (!m) { d->SetError(LLoadString(IDS_GNUPG_ERR_NOMSG)); DecryptStatus(1); } SignEncrypt(d->Sign->Value() != 0, d->Enc->Value() != 0, d->Attach->Value() != 0, [this, m, callback](auto status) { if (!status) { // Send the email.. m->Send(true); // Close the window... d->Ui->Quit(); } // Bypass the normal code DecryptStatus(1); }); } } DecryptStatus(0); } LMessage::Result MailUiGpg::OnEvent(LMessage *Msg) { switch (Msg->Msg()) { case M_GNUPG_KEY_INFO: { d->Inf.Reset( (KeyArr*) Msg->B() ); AddressList *AddrLst = d->GetAddrLst(); if (d->Inf && AddrLst) { int NoKey = 0; List a; if (AddrLst->GetAll(a)) { LHashTbl, GpgConnector::KeyInfo*> Map; for (unsigned i=0; iInf->Length(); i++) { GpgConnector::KeyInfo *ki = &(*d->Inf)[i]; Map.Add(ki->Email, ki); } for (auto i: a) { // Check if this recipient has a key... GpgConnector::KeyInfo *ki = (i->sAddr) ? Map.Find(i->sAddr) : NULL; i->SetInt(FIELD_COLOUR, ki ? cDefaultListItemColour : cError.c32()); i->Update(); if (!ki) NoKey++; } } if (NoKey > 0) d->SetError(LLoadString(IDS_GNUPG_ERR_ONE_OR_MORE)); else if (d->Enc && d->Sign) { if (d->Enc->Value() && d->Sign->Value()) d->SetSuccess(LLoadString(IDS_GNUPG_ENCRYPTED_AND_SIGNED)); else if (d->Enc->Value()) d->SetSuccess(LLoadString(IDS_GNUPG_ENCRYPTED)); else if (d->Sign->Value()) d->SetSuccess(LLoadString(IDS_GNUPG_SIGNED)); else LAssert(0); } else LAssert(0); } else d->SetError("Parameter error."); break; } case M_GNUPG_SIG_CHECK: { Mail *m = d->Ui ? d->Ui->GetItem() : NULL; LAutoPtr Resp((GpgSigCheckResponse*)Msg->A()); if (!Resp || m != (Mail*)Resp->UserValue) { d->SetError("M_GNUPG_SIG_CHECK: bad response"); break; } if (Resp->Error) { d->SetError(Resp->Error); break; } LString Dt = Resp->TimeStamp.Get(); const char *Type = Resp->SignatureMatch ? LLoadString(IDS_GNUPG_GOOD_SIG) : LLoadString(IDS_GNUPG_BAD_SIG); LString Msg; if (Resp->Identity) Msg.Printf(LLoadString(IDS_GNUPG_SIG_MSG), Type, Resp->Identity.Get(), Dt.Get()); else Msg.Printf(LLoadString(IDS_GNUPG_SIG_NO_ID), Type, Dt.Get()); if (Resp->SignatureMatch) d->SetSuccess(Msg); else d->SetError(Msg); break; } case M_GNUPG_DECRYPT: { LAutoPtr Resp((GpgDecryptResponse*)Msg->A()); if (!Resp) { d->SetError(LLoadString(IDS_GNUPG_ERR_INVALID_DECRYPTION)); break; } if (!Resp->Data) { d->SetError(LLoadString(IDS_GNUPG_ERR_NO_OUTPUT)); break; } if (Resp->Error) { d->SetError(Resp->Error); break; } Mail *m = d->Ui->GetItem(); if (!m || m != (Mail*)Resp->UserValue) { d->SetError("Incorrect mail object after decryption."); break; } d->SetSuccess(LLoadString(IDS_GNUPG_DECRYPT_OK)); LDataI *Obj = m->GetObject(); if (!Obj) { d->SetError(LLoadString(IDS_GNUPG_ERR_NOMSG)); break; } // This turns of notification processing, the message is changing // but we don't want it to get dirty. d->Ui->_Running = false; // Unload the attachments, because they will point to stale back-end // objects, that "SetStream" will delete. m->UnloadAttachments(); // Set the stream of the object (which will MIME parse the data, // replacing the MIME Seg tree) Obj->SetStream(Resp->Data); // Re-enable the notifcation processing. d->Ui->_Running = true; // Get the UI to reload and display the object... LArray Items; Items.Add(Obj); m->App->SetContext(_FL); m->App->OnChange(Items, 0); // Change the button to disabled... no need for it anymore. SetCtrlEnabled(IDC_DECRYPT, false); break; } } return LView::OnEvent(Msg); } bool MailUiGpg::Pour(LRegion &r) { LRect lrg = FindLargest(r); if (!lrg.Valid()) return false; int y = LSysFont->GetHeight() + 12; lrg.y2 = lrg.y1 + MIN(y, lrg.Y()) - 1; SetPos(lrg); if (d->Table) { LRect c = GetClient(); c.Inset(PANEL_BORDER_PX, PANEL_BORDER_PX); d->Table->SetPos(c); } return true; } void MailUiGpg::OnCreate() { AttachChildren(); LResources::StyleElement(this); // This is in OnCreate because the CheckSignature needs a valid // view handle to post the message back to us. if (d->Signed) { GpgConnector *Conn = d->App->GetGpgConnector(); if (Conn) { Mail *m = d->Ui->GetItem(); LDataI *obj = m ? m->GetObject() : NULL; LAutoStreamI Msg; if (obj) Msg = obj->GetStream(_FL); if (Msg) { Conn->CheckSignature(this, Msg, (LMessage::Param)m); d->SetWarning(LLoadString(IDS_GNUPG_CHECK_MSG)); } else { d->SetError(LLoadString(IDS_GNUPG_ERR_NOMSG)); } } else { d->NotInstalled(); } } } void MailUiGpg::OnPaint(LSurface *pDC) { LCssTools Tools(this); LRect c = GetClient(); Tools.PaintBorder(pDC, c); Tools.PaintPadding(pDC, c); Tools.PaintContent(pDC, c); } diff --git a/Code/Exp_Scribe.cpp b/Code/Exp_Scribe.cpp --- a/Code/Exp_Scribe.cpp +++ b/Code/Exp_Scribe.cpp @@ -1,661 +1,927 @@ #include "Scribe.h" #include "lgi/common/List.h" #include "lgi/common/ProgressDlg.h" #include "lgi/common/Store3.h" #include "lgi/common/LgiRes.h" #include "lgi/common/FileSelect.h" #include "ScribeFolderSelect.h" #include "../Resources/resdefs.h" +#include "FolderTask.h" -class ScribeExport : public LDialog, public LDataEventsI +/* + + int CountItems(ScribeFolder *f, bool Children) + { + int Status = 0; + + if (f && f->Store) + { + for (StorageItem *i = f->Store->GetChild(); i; i = i->GetNext()) + { + if (i->GetType() == MAGIC_MAIL || + i->GetType() == MAGIC_CONTACT) + { + Status++; + } + } + + if (Children) + { + for (ScribeFolder *c = f->GetChildFolder(); c; c = c->GetNextFolder()) + { + if ((!Spam || c != Spam) && + (!Trash || c != Trash)) + { + Status += CountItems(c, Children); + } + } + } + } + + return Status; + } + +*/ + +struct ExportParams +{ + bool AllFolders = false; + bool ExceptTrashSpam = false; + LString DestFolders; // Mail3 path for dest folders; + LString DestPath; // Sub folder path for export + LString::Array SrcPaths; +}; + +struct Mail3Folders { ScribeWnd *App = NULL; - LAutoPtr Folders; - ScribeFolder *Mailbox = NULL; - LList *Lst = NULL; + LAutoPtr Store; + LAutoPtr Root; + + Mail3Folders(ScribeWnd *app) : App(app) + { + int asd=0; + } + + Mail3Folders(Mail3Folders &src) + { + App = src.App; + + if (src) + { + Store = src.Store; + Root = src.Root; + } + else LAssert(!"Src not loaded."); + } + + operator bool() + { + return Store != NULL && Root != NULL; + } + + void LoadFolders(const char *FilePath) + { + if (!Store) + Store.Reset(App->CreateDataStore(FilePath, true)); + + if (Store && !Root) + { + if (Root.Reset(new ScribeFolder)) + { + Root->App = App; + Root->SetObject(Store->GetRoot(), false, _FL); + } + } + } + + void Unload() + { + Root.Reset(); + Store.Reset(); + } + + ScribeFolder *GetFolder(LString Path, Store3ItemTypes CreateItemType = MAGIC_NONE) + { + if (!Root) + { + LAssert(!"No root loaded."); + return NULL; + } + + auto parts = Path.SplitDelimit("/"); + ScribeFolder *f = Root; + for (auto p: parts) + { + auto c = f->GetSubFolder(p); + if (!c) + { + if (CreateItemType != MAGIC_NONE) + { + c = f->CreateSubDirectory(p, CreateItemType); + if (!c) + return false; + } + else return NULL; + } + + f = c; + } + + return f; + } +}; + +struct ScribeExportTask : public FolderTask +{ + int FolderLoadErrors = 0; + int MailCreated = 0; + int MailSkipped = 0; + int MailErrors = 0; + int ContactCreated = 0; + int ContactSkipped = 0; + int ContactErrors = 0; + + LMailStore *SrcStore = NULL; // Source data store + Mail3Folders Dst; ScribeFolder *Spam = NULL; ScribeFolder *Trash = NULL; + ExportParams Params; + + // Working state, used to iterate over the exporting process while + // being able to yield to the OS at regular intervals. + enum ExportState + { + ExpNone, + ExpGetNext, + ExpLoadFolders, + ExpItems, + ExpFinished, + } State = ExpNone; + + LString::Array InputPaths; + ScribeFolder *SrcFolder = NULL; + LArray SrcItems; + ScribeFolder *DstFolder = NULL; + LDataFolderI *DstObj = NULL; + LDataStoreI *DstStore = NULL; + LHashTbl,LDataI*> DstMsgIds; + + ScribeExportTask(struct ScribeExportDlg *dlg); + + LString ContactKey(Contact *c); + bool TimeSlice(); + bool CopyAttachments(LDataI *outMail, LDataPropI *outSeg, LDataPropI *inSeg, LString &err); + + LString MakePath(LString path) + { + LString sep; + auto p = Params.DestPath.SplitDelimit(sep); + p += path.Strip(sep).SplitDelimit(sep).Slice(1); + return sep + sep.Join(p); + } + + void CollectPaths(ScribeFolder *f, LString::Array &paths) + { + paths.Add(f->GetPath()); + for (auto c = f->GetChildFolder(); c; c = c->GetNextFolder()) + CollectPaths(c, paths); + } + + void OnComplete() + { + LgiMsg( this, + "Mail export complete.\n" + "\n" + " Email: %i created, %i already exist, %i errors\n" + " Contacts: %i created, %i already exist, %i errors", + "Export", + MB_OK, + MailCreated, + MailSkipped, + MailErrors, + ContactCreated, + ContactSkipped, + ContactErrors); + } +}; + +struct ScribeExportDlg : public LDialog, public LDataEventsI +{ + ScribeWnd *App = NULL; + LMailStore *SrcStore = NULL; + LList *Lst = NULL; + ExportParams Params; + Mail3Folders Dst; + + ScribeExportDlg(ScribeWnd *app, LMailStore *srcStore) : + SrcStore(srcStore), + Dst(app) + { + SetParent(App = app); + + if (!SrcStore) + SrcStore = App->GetDefaultMailStore(); + + if (LoadFromResource(IDD_SCRIBE_EXPORT)) + { + MoveToCenter(); + // EnableCtrls(false); + GetViewById(IDC_SRC_FOLDERS, Lst); + + LVariant s; + if (Lst && App->GetOptions()->GetValue(OPT_ScribeExpSrcPaths, s) && s.Str()) + { + Params.SrcPaths = LString(s.Str()).SplitDelimit(":"); + for (auto p: Params.SrcPaths) + Lst->Insert(new LListItem(p)); + + Lst->ResizeColumnsToContent(); + } + + if (App->GetOptions()->GetValue(OPT_ScribeExpDstPath, s) && + ValidStr(s.Str())) + SetCtrlName(IDC_FOLDER, s.Str()); + else + SetCtrlName(IDC_FOLDER, "/"); + + LVariant n; + if (App->GetOptions()->GetValue(OPT_ScribeExpAll, n)) + SetCtrlValue(IDC_ALL, n.CastInt32()); + + if (App->GetOptions()->GetValue(OPT_ScribeExpExclude, n)) + SetCtrlValue(IDC_NO_SPAM_TRASH, n.CastInt32()); + else + SetCtrlValue(IDC_NO_SPAM_TRASH, true); + + if (App->GetOptions()->GetValue(OPT_ScribeExpFolders, s) && s.Str()) + SetCtrlName(IDC_DEST, s.Str()); + + OnAll(); + } + } void OnNew(LDataFolderI *parent, LArray &new_items, int pos, bool is_new) { } bool OnDelete(LDataFolderI *parent, LArray &items) { return true; } bool OnMove(LDataFolderI *new_parent, LDataFolderI *old_parent, LArray &items) { return true; } bool OnChange(LArray &items, int FieldHint) { return true; } -public: - bool AllFolders = false; - bool ExceptTrashSpam = false; - LString DestPath; - LString::Array SrcPaths; - - int MailCreated = 0; - int MailSkipped = 0; - int MailErrors = 0; - int ContactCreated = 0; - int ContactSkipped = 0; - int ContactErrors = 0; - - ScribeExport(ScribeWnd *app) - { - SetParent(App = app); - if (LoadFromResource(IDD_SCRIBE_EXPORT)) - { - MoveToCenter(); - // EnableCtrls(false); - GetViewById(IDC_SRC_FOLDERS, Lst); - - LVariant s; - if (Lst && App->GetOptions()->GetValue(OPT_ScribeExpSrcPaths, s) && s.Str()) - { - SrcPaths = LString(s.Str()).SplitDelimit(":"); - for (auto p: SrcPaths) - Lst->Insert(new LListItem(p)); - - Lst->ResizeColumnsToContent(); - } - - if (App->GetOptions()->GetValue(OPT_ScribeExpDstPath, s) && - ValidStr(s.Str())) - { - SetCtrlName(IDC_FOLDER, s.Str()); - } - else - { - SetCtrlName(IDC_FOLDER, "/"); - } - - LVariant n; - if (App->GetOptions()->GetValue(OPT_ScribeExpAll, n)) - { - SetCtrlValue(IDC_ALL, n.CastInt32()); - } - - if (App->GetOptions()->GetValue(OPT_ScribeExpExclude, n)) - { - SetCtrlValue(IDC_NO_SPAM_TRASH, n.CastInt32()); - } - else - { - SetCtrlValue(IDC_NO_SPAM_TRASH, true); - } - - if (App->GetOptions()->GetValue(OPT_ScribeExpFolders, s) && s.Str()) - { - SetCtrlName(IDC_DEST, s.Str()); - LoadFolders(); - } - - OnAll(); - } - } - - ~ScribeExport() - { - UnloadFolders(); - SrcPaths.DeleteArrays(); - } - void OnAll() { SetCtrlEnabled(IDC_SRC_FOLDERS, !GetCtrlValue(IDC_ALL)); SetCtrlEnabled(IDC_ADD_SRC_FOLDER, !GetCtrlValue(IDC_ALL)); SetCtrlEnabled(IDC_DEL_SRC_FOLDER, !GetCtrlValue(IDC_ALL)); } - /* - void EnableCtrls(bool e) - { - SetCtrlEnabled(IDC_DEST, !e); - SetCtrlEnabled(IDC_ALL, e); - SetCtrlEnabled(IDC_NO_SPAM_TRASH, e); - SetCtrlEnabled(IDC_SRC_FOLDERS, e); - SetCtrlEnabled(IDC_ADD_SRC_FOLDER, e); - SetCtrlEnabled(IDC_DEL_SRC_FOLDER, e); - SetCtrlEnabled(IDC_FOLDER, e); - SetCtrlEnabled(IDC_SET_FOLDER, e); - SetCtrlEnabled(IDOK, e); - } - */ - - void UnloadFolders() - { - DeleteObj(Mailbox); - Folders.Reset(); - } - - void LoadFolders() - { - if (!Folders) - { - auto path = GetCtrlName(IDC_DEST); - Folders.Reset(App->CreateDataStore(path, true)); - } - - if (Folders) - { - Mailbox = new ScribeFolder; - if (Mailbox) - { - Mailbox->App = App; - Mailbox->SetObject(Folders->GetRoot(), false, _FL); - } - } - } - - ScribeFolder *GetFolder(char *Path, ScribeFolder *CreateAs = 0) - { - ScribeFolder *f = 0; - - if (Path && Mailbox) - { - LToken t(Path, "/"); - f = Mailbox; - for (unsigned i=0; iGetChildFolder(); Child; Child = Child->GetNextFolder()) - { - auto n = Child->GetName(true); - if (n.Equals(t[i])) - { - f = Child; - Found = true; - break; - } - } - - if (!Found) - { - if (CreateAs) - { - f = f->CreateSubDirectory(t[i], CreateAs->GetItemType()); - if (!f) - break; - } - else - { - f = 0; - break; - } - } - } - } - - return f; - } - - LString ContactKey(Contact *c) - { - LString p; - const char *f = 0, *l = 0; - auto e = c->GetAddrAt(0); - c->Get(OPT_First, f); - c->Get(OPT_Last, l); - p.Printf("%s,%s,%s", e.Get(), f, l); - return p; - } - - #define ExportFolderStatus(b) \ - { if (onStatus) onStatus(b); \ - return; } - - void ExportFolder( LString ToPath, - LString FromPath, - bool Children, - LProgressDlg *Prog, - std::function onStatus) - { - if (!ToPath || !FromPath) - ExportFolderStatus(false); - - ScribeFolder *From = App->GetFolder(FromPath); - if (!From) - ExportFolderStatus(false); - - if (!((!Spam || From != Spam) && - (!Trash || From != Trash))) - ExportFolderStatus(false); - - ScribeFolder *To = GetFolder(ToPath, From); - if (!To) - ExportFolderStatus(false); - - bool FromLoaded = From->IsLoaded(); - bool ToLoaded = From->IsLoaded(); - - auto ProcessItem = [this, To, Prog, FromPath, From, FromLoaded, ToLoaded, Children, ToPath, onStatus]() - { - bool Status = true; - - switch ((uint32_t)To->GetItemType()) - { - case MAGIC_MAIL: - { - if (Prog) - Prog->SetDescription(FromPath); - - LHashTbl,Mail*> ToMsgs; - for (auto t: To->Items) - { - Mail *m = t->IsMail(); - if (m) - { - auto Id = m->GetMessageId(true); - if (Id) - { - ToMsgs.Add(Id, m); - } - } - } - - int InitMailErrors = MailErrors; - - for (auto t: From->Items) - { - if (Prog && Prog->IsCancelled()) - break; - - Mail *m = t->IsMail(); - if (m) - { - auto Id = m->GetMessageId(true); - if (Id) - { - if (!ToMsgs.Find(Id)) - { - // Create new mail... - Mail *n = new Mail(App); - if (n) - { - *n = (Thing&)*m; - n->SetParentFolder(To); - n->SetObject(To->GetObject()->GetStore()->Create(MAGIC_MAIL), false, _FL); - if (n->GetObject()) - { - MailCreated++; - - // Now create all the attachments - List Att; - if (m->GetAttachments(&Att)) - { - for (auto OldAttachment: Att) - { - Attachment *NewAttachment = new Attachment(m->App, OldAttachment); - if (NewAttachment) - { - n->AttachFile(NewAttachment); - NewAttachment->SetObject(n->GetObject()->GetStore()->Create(MAGIC_ATTACHMENT), false, _FL); - } - } - } - } - else MailErrors++; - - } - else MailErrors++; - } - else MailSkipped++; - } - else MailErrors++; - } - - if (Prog) - Prog->Value(Prog->Value() + 1); - } - - Status |= MailErrors == InitMailErrors; - break; - } - case MAGIC_CONTACT: - { - LHashTbl,Contact*> ToContacts; - for (auto t: To->Items) - { - Contact *c = t->IsContact(); - if (c) - { - auto k = ContactKey(c); - if (k) - ToContacts.Add(k, c); - } - } - - int InitContactErrors = ContactErrors; - uint64 Last = LCurrentTime(); - for (auto t: From->Items) - { - if (Prog && Prog->IsCancelled()) - break; - - Contact *c = t->IsContact(); - if (c) - { - auto k = ContactKey(c); - if (k) - { - if (!ToContacts.Find(k)) - { - Contact *n = new Contact(App); - if (n) - { - *n = (Thing&)*c; - n->SetParentFolder(To); - } - else ContactErrors++; - } - else ContactSkipped++; - } - else ContactErrors++; - } - - if (Prog) - Prog->Value(Prog->Value() + 1); - } - - Status |= ContactErrors == InitContactErrors; - break; - } - - if (!FromLoaded) - { - From->UnloadThings(); - } - if (!ToLoaded) - { - To->UnloadThings(); - } - - if (Children) - { - char t[256]; - char f[256]; - LString n; - - for (ScribeFolder *c = From->GetChildFolder(); c && (!Prog || !Prog->IsCancelled()); c = c->GetNextFolder()) - { - n = c->GetName(true); - if (n) - { - strcpy_s(t, sizeof(t), ToPath); - char *e = t + strlen(t) - 1; - if (*e++ != '/') *e++ = '/'; - strcpy_s(e, sizeof(t)-(e-t), n); - - strcpy_s(f, sizeof(f), FromPath); - e = f + strlen(f) - 1; - if (*e++ != '/') *e++ = '/'; - strcpy_s(e, sizeof(f)-(e-f), n); - - ExportFolder(t, f, true, Prog, [](auto ok){}); - } - } - } - } - }; - - To->LoadThings(NULL, [From, ProcessItem](auto status) - { - From->LoadThings(NULL, [ProcessItem](auto status) - { - ProcessItem(); - }); - }); - } - int OnNotify(LViewI *c, LNotification n) { switch (c->GetId()) { case IDC_ALL: { OnAll(); break; } case IDC_SET_DEST: { auto s = new LFileSelect(this); s->Type("Scribe Folders", "*.mail3"); s->Type("All Files", LGI_ALL_FILES); s->Open([this](auto dlg, auto status) { if (status) - { - UnloadFolders(); SetCtrlName(IDC_DEST, dlg->Name()); - LoadFolders(); - } delete dlg; }); break; } case IDC_ADD_SRC_FOLDER: { if (!Lst) break; auto s = new FolderDlg(this, App); s->DoModal([this, s](auto dlg, auto status) { if (status && ValidStr(s->Get())) { bool Has = false; - for (auto n : *Lst) + for (auto n: *Lst) { - const char *p = n->GetText(0); - if (p && _stricmp(p, s->Get()) == 0) + if (Stricmp(n->GetText(), s->Get()) == 0) { Has = true; break; } } if (!Has) { - LListItem *i = new LListItem; - if (i) - { - i->SetText(s->Get()); - Lst->Insert(i); - Lst->ResizeColumnsToContent(); - } + Lst->Insert(new LListItem(s->Get())); + Lst->ResizeColumnsToContent(); } } delete dlg; }); break; } case IDC_DEL_SRC_FOLDER: { if (Lst) { List i; if (Lst->GetSelection(i)) { i.DeleteObjects(); } } break; } case IDC_SET_FOLDER: { - LoadFolders(); - if (!Mailbox) - break; + if (!Dst) + Dst.LoadFolders(GetCtrlName(IDC_DEST)); - auto s = new FolderDlg(this, App, MAGIC_NONE, Mailbox); - s->DoModal([this, s](auto dlg, auto ctrlId) + if (Dst.Root) { - if (ctrlId) - SetCtrlName(IDC_FOLDER, s->Get()); - delete dlg; - }); + auto s = new FolderDlg(this, App, MAGIC_NONE, Dst.Root); + s->DoModal([this, s](auto dlg, auto ctrlId) + { + if (ctrlId) + SetCtrlName(IDC_FOLDER, s->Get()); + delete dlg; + }); + } + else LgiMsg(this, "Couldn't load mail3 store.", AppName); break; } case IDOK: { - LoadFolders(); + Params.AllFolders = GetCtrlValue(IDC_ALL) != 0; + Params.ExceptTrashSpam = GetCtrlValue(IDC_NO_SPAM_TRASH) != 0; + Params.DestPath = GetCtrlName(IDC_FOLDER); + Params.DestFolders = GetCtrlName(IDC_DEST); - AllFolders = GetCtrlValue(IDC_ALL) != 0; - if ((ExceptTrashSpam = GetCtrlValue(IDC_NO_SPAM_TRASH) != 0)) + if (Lst) { - Spam = App->GetFolder("/Spam"); - Trash = App->GetFolder(FOLDER_TRASH); + for (auto i: *Lst) + Params.SrcPaths.Add(i->GetText()); } - DestPath = GetCtrlName(IDC_FOLDER); - if (Lst) - { - for (auto i : *Lst) - { - SrcPaths.Add(NewStr(i->GetText(0))); - } - } + if (!Dst) + Dst.LoadFolders(Params.DestFolders); // fall through } case IDCANCEL: { LVariant v; if (Lst) { LStringPipe p; int n=0; for (auto i : *Lst) { p.Print("%s%s", n ? (char*)":" : (char*) "", i->GetText(0)); } char *s = p.NewStr(); if (s) { App->GetOptions()->SetValue(OPT_ScribeExpSrcPaths, v = s); DeleteArray(s); } } App->GetOptions()->SetValue(OPT_ScribeExpDstPath, v = GetCtrlName(IDC_FOLDER)); App->GetOptions()->SetValue(OPT_ScribeExpAll, v = (int)GetCtrlValue(IDC_ALL)); App->GetOptions()->SetValue(OPT_ScribeExpExclude, v = (int)GetCtrlValue(IDC_NO_SPAM_TRASH)); App->GetOptions()->SetValue(OPT_ScribeExpFolders, v = GetCtrlName(IDC_DEST)); EndModal(c->GetId() == IDOK); } } return 0; } +}; - int CountItems(ScribeFolder *f, bool Children) +ScribeExportTask::ScribeExportTask(ScribeExportDlg *dlg) : + FolderTask(dlg->Dst.Root, LAutoPtr(NULL), NULL, NULL), + Dst(dlg->Dst), + SrcStore(dlg->SrcStore) +{ + SetDescription("Initializing..."); + SetType("Folders"); + + LAssert(SrcStore); + + Params = dlg->Params; + if (Params.ExceptTrashSpam) { - int Status = 0; - /* - if (f && f->Store) + Spam = App->GetFolder("/Spam"); + Trash = App->GetFolder(FOLDER_TRASH); + } + + // Work out the folders we need to operate on: + if (Params.AllFolders) + CollectPaths(SrcStore->Root, InputPaths); + else + InputPaths = Params.SrcPaths; + SetRange(InputPaths.Length()); + + // Kick off the processing... + State = ExpGetNext; + SetPulse(PULSE_MS); + SetAlwaysOnTop(true); +} + +LString ScribeExportTask::ContactKey(Contact *c) +{ + LString p; + const char *f = 0, *l = 0; + auto e = c->GetAddrAt(0); + c->Get(OPT_First, f); + c->Get(OPT_Last, l); + p.Printf("%s,%s,%s", e.Get(), f, l); + return p; +} + +#define ExportFolderStatus(b) \ + { if (onStatus) onStatus(b); \ + return; } + +/* +void ScribeExportTask::ExportFolder(LString ToPath, + LString FromPath, + bool Children, + LProgressDlg *Prog, + std::function onStatus) +{ + if (!ToPath || !FromPath) + ExportFolderStatus(false); + + ScribeFolder *From = App->GetFolder(FromPath); + if (!From) + ExportFolderStatus(false); + + if (!((!Spam || From != Spam) && + (!Trash || From != Trash))) + ExportFolderStatus(false); + + ScribeFolder *To = GetFolder(ToPath, From); + if (!To) + ExportFolderStatus(false); + + bool FromLoaded = From->IsLoaded(); + bool ToLoaded = From->IsLoaded(); + + auto ProcessItem = [this, To, Prog, FromPath, From, FromLoaded, ToLoaded, Children, ToPath, onStatus]() + { + bool Status = true; + + + if (!FromLoaded) { - for (StorageItem *i = f->Store->GetChild(); i; i = i->GetNext()) + From->UnloadThings(); + } + if (!ToLoaded) + { + To->UnloadThings(); + } + + if (Children) + { + char t[256]; + char f[256]; + LString n; + + for (ScribeFolder *c = From->GetChildFolder(); c && (!Prog || !Prog->IsCancelled()); c = c->GetNextFolder()) { - if (i->GetType() == MAGIC_MAIL || - i->GetType() == MAGIC_CONTACT) + n = c->GetName(true); + if (n) { - Status++; - } - } + strcpy_s(t, sizeof(t), ToPath); + char *e = t + strlen(t) - 1; + if (*e++ != '/') *e++ = '/'; + strcpy_s(e, sizeof(t)-(e-t), n); - if (Children) - { - for (ScribeFolder *c = f->GetChildFolder(); c; c = c->GetNextFolder()) - { - if ((!Spam || c != Spam) && - (!Trash || c != Trash)) - { - Status += CountItems(c, Children); - } + strcpy_s(f, sizeof(f), FromPath); + e = f + strlen(f) - 1; + if (*e++ != '/') *e++ = '/'; + strcpy_s(e, sizeof(f)-(e-f), n); + + ExportFolder(t, f, true, Prog, [](auto ok){}); } } } - */ - return Status; - } -}; + }; + + To->LoadThings(NULL, [From, ProcessItem](auto status) + { + From->LoadThings(NULL, [ProcessItem](auto status) + { + ProcessItem(); + }); + }); +} + +bool ScribeExportTask::ExportThing() +{ + switch ((uint32_t)SrcFolder->GetItemType()) + { + case MAGIC_MAIL: + { + int InitMailErrors = MailErrors; + + for (auto t: From->Items) + { + if (Prog && Prog->IsCancelled()) + break; + + Mail *m = t->IsMail(); + if (m) + { + auto Id = m->GetMessageId(true); + if (Id) + { + if (!ToMsgs.Find(Id)) + { + // Create new mail... + Mail *n = new Mail(App); + if (n) + { + *n = (Thing&)*m; + n->SetParentFolder(To); + n->SetObject(To->GetObject()->GetStore()->Create(MAGIC_MAIL), false, _FL); + if (n->GetObject()) + { + MailCreated++; -void ExportScribe(ScribeWnd *App) -{ - auto Dlg = new ScribeExport(App); - Dlg->DoModal([Dlg, App](auto dlg, auto ctrlId) - { - if (ctrlId) + // Now create all the attachments + List Att; + if (m->GetAttachments(&Att)) + { + for (auto OldAttachment: Att) + { + Attachment *NewAttachment = new Attachment(m->App, OldAttachment); + if (NewAttachment) + { + n->AttachFile(NewAttachment); + NewAttachment->SetObject(n->GetObject()->GetStore()->Create(MAGIC_ATTACHMENT), false, _FL); + } + } + } + } + else MailErrors++; + + } + else MailErrors++; + } + else MailSkipped++; + } + else MailErrors++; + } + + if (Prog) + Prog->Value(Prog->Value() + 1); + } + + Status |= MailErrors == InitMailErrors; + break; + } + case MAGIC_CONTACT: { + LHashTbl,Contact*> ToContacts; + for (auto t: To->Items) + { + Contact *c = t->IsContact(); + if (c) + { + auto k = ContactKey(c); + if (k) + ToContacts.Add(k, c); + } + } + + int InitContactErrors = ContactErrors; + uint64 Last = LCurrentTime(); + for (auto t: From->Items) { - LProgressDlg Prog(App); - Prog.SetDescription("Initializing..."); - Prog.SetType("items"); + if (Prog && Prog->IsCancelled()) + break; + + Contact *c = t->IsContact(); + if (c) + { + auto k = ContactKey(c); + if (k) + { + if (!ToContacts.Find(k)) + { + Contact *n = new Contact(App); + if (n) + { + *n = (Thing&)*c; + n->SetParentFolder(To); + } + else ContactErrors++; + } + else ContactSkipped++; + } + else ContactErrors++; + } + + if (Prog) + Prog->Value(Prog->Value() + 1); + } + + Status |= ContactErrors == InitContactErrors; + break; + } +} +*/ + +bool ScribeExportTask::TimeSlice() +{ + if (IsCancelled()) + return false; + + switch (State) + { + case ExpGetNext: + { + if (InputPaths.Length() == 0) + { + State = ExpFinished; + break; + } + + // Load up the next SrcFolder... + auto src = InputPaths[0]; + InputPaths.DeleteAt(0, true); + + auto dst = MakePath(src); + SrcFolder = App->GetFolder(src, SrcStore); + if (!SrcFolder) + { + FolderLoadErrors++; + return true; + } - auto Ms = App->GetDefaultMailStore(); - if (!Ms) - return; + DstStore = NULL; + DstFolder = Dst.GetFolder(dst, SrcFolder->GetItemType()); + if (!DstFolder) + { + return true; + } + + State = ExpLoadFolders; + SrcFolder->LoadThings(this, [this](auto status) + { + if (status == Store3Success) + { + // Load a list of things to process... + SrcItems.Empty(); + auto SrcObj = dynamic_cast(SrcFolder->GetObject()); + if (SrcObj) + { + auto &c = SrcObj->Children(); + for (auto t = c.First(); t; t = c.Next()) + SrcItems.Add(t); + } + + DstFolder->LoadThings(this, [this](auto status) + { + if (status == Store3Success) + { + State = ExpItems; + SetDescription(DstFolder->GetPath()); - int Items = 0; - if (Dlg->AllFolders) - { - Items += Dlg->CountItems(Ms->Root, true); + if (DstFolder->GetObject()) + DstObj = dynamic_cast(DstFolder->GetObject()); + else + LAssert(!"No object?"); + + if (DstObj) + DstStore = DstObj->GetStore(); + else + LAssert(!"No object?"); + + DstMsgIds.Empty(); + if (DstFolder->GetItemType() == MAGIC_MAIL) + { + // Make a map of destination folder message IDs + if (DstObj) + { + auto &c = DstObj->Children(); + for (auto t = c.First(); t; t = c.Next()) + { + if (t->Type() != MAGIC_MAIL) + continue; + + auto Id = t->GetStr(FIELD_MESSAGE_ID); + if (Id) + DstMsgIds.Add(Id, t); + } + } + } + } + else + State = ExpGetNext; + }); } else { - for (auto path: Dlg->SrcPaths) - Items += Dlg->CountItems(App->GetFolder(path), false); - } - Prog.SetRange(Items); + State = ExpGetNext; + } + }); + break; + } + case ExpLoadFolders: + { + // No-op, but we should probably time out... + break; + } + case ExpItems: + { + if (!SrcFolder || !DstFolder || !DstStore) + { + State = ExpGetNext; + break; + } - if (Dlg->AllFolders) + auto Trans = DstStore->StartTransaction(); + + auto StartTs = LCurrentTime(); + int Processed = 0; + while ( SrcItems.Length() > 0 && + LCurrentTime() - StartTs < WORK_SLICE_MS) + { + auto in = SrcItems[0]; + SrcItems.DeleteAt(0); + + switch (in->Type()) { - Dlg->ExportFolder(Dlg->DestPath, "/", true, &Prog, NULL); - } - else - { - for (unsigned i=0; iSrcPaths.Length() && !Prog.IsCancelled(); i++) + case MAGIC_MAIL: { - char Dest[256]; - strcpy_s(Dest, sizeof(Dest), Dlg->DestPath); - char *e = Dest + strlen(Dest) - 1; - if (*e == '/') *e = 0; - strcat(Dest, Dlg->SrcPaths[i]); + auto Id = in->GetStr(FIELD_MESSAGE_ID); + if (!Id) + { + MailErrors++; + break; + } + + if (DstMsgIds.Find(Id)) + { + MailSkipped++; + break; + } + + // Create new mail... + auto outMail = DstStore->Create(MAGIC_MAIL); + outMail->CopyProps(*in); + + // Now create all the attachments + auto inSeg = dynamic_cast(in->GetObj(FIELD_MIME_SEG)); + LDataI *outSeg = NULL; + if (inSeg) + { + outSeg = DstStore->Create(MAGIC_ATTACHMENT); + if (!outSeg) + MailErrors++; + else + { + outSeg->CopyProps(*inSeg); - Dlg->ExportFolder(Dest, Dlg->SrcPaths[i], false, &Prog, NULL); + auto outMime = outSeg->GetStr(FIELD_MIME_TYPE); + if (!outMime) + { + auto hdrs = outSeg->GetStr(FIELD_INTERNET_HEADER); + if (!hdrs) + { + // This is going to cause an assert later + outSeg->SetStr(FIELD_MIME_TYPE, sAppOctetStream); + LgiTrace("%s:%i - Setting default mime on %p\n", _FL, outSeg); + } + } + + LgiTrace("%s:%i - Setting root seg: %p\n", _FL, outSeg); + if (outMail->SetObj(FIELD_MIME_SEG, outSeg) < Store3Delayed) + MailErrors++; + else + { + LString err; + if (!CopyAttachments(outMail, outSeg, inSeg, err)) + MailErrors++; + else + MailCreated++; + } + } + } + else MailCreated++; + + outMail->Save(DstFolder->GetObject()); + break; + } + default: + { + LgiTrace("%s:%i - Unhandled object type.\n", _FL); + break; } } + + Processed++; + + } + + if (SrcItems.Length() == 0) + { + SrcFolder = NULL; + DstFolder = NULL; + DstStore = NULL; + State = ExpGetNext; + (*this)++; // move progress... } - LgiMsg( App, - "Mail export complete.\n" - "\n" - " Email: %i created, %i already exist, %i errors\n" - " Contacts: %i created, %i already exist, %i errors", - "Export", - MB_OK, - Dlg->MailCreated, - Dlg->MailSkipped, - Dlg->MailErrors, - Dlg->ContactCreated, - Dlg->ContactSkipped, - Dlg->ContactErrors - ); + LgiTrace("Processed: %i\n", Processed); + break; + } + case ExpFinished: + { + return false; + } + } + + return true; +} + +// This should copy all the child objects of 'inSeg' to new child objects of 'outSeg' +bool ScribeExportTask::CopyAttachments(LDataI *outMail, LDataPropI *outSeg, LDataPropI *inSeg, LString &err) +{ + #define ERR(str) \ + { err = str; return false; } + if (!outMail || !outSeg || !inSeg) + ERR("param error"); + + auto children = inSeg->GetList(FIELD_MIME_SEG); + if (!children) + return true; // Nothing to copy... + + for (auto i = children->First(); i; i = children->Next()) + { + auto inMime = i->GetStr(FIELD_MIME_TYPE); + if (!inMime) + continue; + + auto o = outMail->GetStore()->Create(MAGIC_ATTACHMENT); + if (!o) + ERR("couldn't create attachment"); + + if (!o->CopyProps(*i)) + ERR("copy attachment properties failed"); + + auto outData = dynamic_cast(outSeg); + if (!outData) + ERR("outSeg isn't a LDataI object"); + + if (!o->Save(outData)) + ERR("failed to save attachment to output mail"); + + if (!CopyAttachments(outMail, o, i, err)) + return false; // but leave the error message untouched. + } + + return true; +} + +void ExportScribe(ScribeWnd *App, LMailStore *Store) +{ + auto Dlg = new ScribeExportDlg(App, Store); + Dlg->DoModal([Dlg, App](auto dlg, auto ctrlId) + { + if (ctrlId && Dlg->Dst) + { + new ScribeExportTask(Dlg); } delete dlg; }); } diff --git a/Code/FolderTask.h b/Code/FolderTask.h new file mode 100644 --- /dev/null +++ b/Code/FolderTask.h @@ -0,0 +1,80 @@ +#pragma once + +class FolderTask : public LProgressDlg +{ +protected: + ScribeWnd *App = NULL; + ScribeFolder *Folder = NULL; + + LString MimeType; + + LAutoPtr Stream; + + ThingType::IoProgress Status; + ThingType::IoProgressCallback onComplete; + +public: + // Minimum amount of time to do work. + constexpr static int WORK_SLICE_MS = 130; + // This should be larger then WORK_SLICE_MS to allow message loop to process + constexpr static int PULSE_MS = 200; + + FolderTask( ScribeFolder *folder, + LAutoPtr stream, + LString mimeType, + ThingType::IoProgressCallback cb) : + LProgressDlg(folder->App), + Folder(folder), + Stream(stream), + MimeType(mimeType), + onComplete(cb), + Status(Store3Success) + { + App = Folder->App; + Ts = LCurrentTime(); + SetParent(Folder->GetTree()); + SetPulse(PULSE_MS); + SetAlwaysOnTop(true); + + App->OnFolderTask(this, true); + } + + virtual ~FolderTask() + { + App->OnFolderTask(this, false); + + if (onComplete) + onComplete(&Status, Stream); + } + + bool OnRequestClose(bool OsClose) + { + return true; + } + + void OnPulse() + { + LProgressDlg::OnPulse(); + + // We aren't checking IsCancelled() here to allow the + // TimeSlice implementation a chance to clean up anything relevant + // before returning false to this caller. + auto StartTs = LCurrentTime(); + while ((LCurrentTime() - StartTs) < WORK_SLICE_MS) + { + if (!TimeSlice()) + { + Quit(); + break; + } + } + } + + /// This should use around WORK_SLICE_MS of time and then + /// \returns true if more work to do or false if finished. + /// Do check for IsCancelled() while doing work and return + /// false. + virtual bool TimeSlice() = 0; +}; + + diff --git a/Code/ObjectInspector.cpp b/Code/ObjectInspector.cpp --- a/Code/ObjectInspector.cpp +++ b/Code/ObjectInspector.cpp @@ -1,400 +1,400 @@ #include "lgi/common/Lgi.h" #include "Scribe.h" #include "ObjectInspector.h" #define DefFld(type, name) { type, #name, name } struct Field { LVariantType Type; const char *Name; int Id; }; static Field MailProps[] = { DefFld(GV_INT64, FIELD_STORE_TYPE), DefFld(GV_INT64, FIELD_SIZE), DefFld(GV_INT64, FIELD_LOADED), DefFld(GV_INT64, FIELD_PRIORITY), DefFld(GV_INT64, FIELD_FLAGS), DefFld(GV_INT64, FIELD_DONT_SHOW_PREVIEW), DefFld(GV_INT64, FIELD_ACCOUNT_ID), DefFld(GV_INT64, FIELD_COLOUR), DefFld(GV_STRING, FIELD_DEBUG), DefFld(GV_STRING, FIELD_SUBJECT), DefFld(GV_STRING, FIELD_CHARSET), DefFld(GV_STRING, FIELD_TEXT), DefFld(GV_STRING, FIELD_HTML_CHARSET), DefFld(GV_STRING, FIELD_ALTERNATE_HTML), DefFld(GV_STRING, FIELD_LABEL), DefFld(GV_STRING, FIELD_INTERNET_HEADER), DefFld(GV_STRING, FIELD_REFERENCES), DefFld(GV_STRING, FIELD_FWD_MSG_ID), DefFld(GV_STRING, FIELD_BOUNCE_MSG_ID), DefFld(GV_STRING, FIELD_SERVER_UID), DefFld(GV_STRING, FIELD_MESSAGE_ID), DefFld(GV_DATETIME, FIELD_DATE_SENT), DefFld(GV_DATETIME, FIELD_DATE_RECEIVED), }; static Field MimeSegProps[] = { DefFld(GV_INT64, FIELD_STORE_TYPE), DefFld(GV_INT64, FIELD_SIZE), DefFld(GV_STRING, FIELD_CHARSET), DefFld(GV_STRING, FIELD_NAME), DefFld(GV_STRING, FIELD_MIME_TYPE), DefFld(GV_STRING, FIELD_CONTENT_ID), DefFld(GV_STRING, FIELD_INTERNET_HEADER), DefFld(GV_STREAM, FIELD_ATTACHMENTS_DATA), }; static Field CalendarProps[] = { DefFld(GV_INT64, FIELD_STORE_TYPE), DefFld(GV_INT64, FIELD_CAL_TYPE), DefFld(GV_INT64, FIELD_CAL_COMPLETED), DefFld(GV_INT64, FIELD_CAL_SHOW_TIME_AS), DefFld(GV_INT64, FIELD_CAL_RECUR), DefFld(GV_INT64, FIELD_CAL_RECUR_FREQ), DefFld(GV_INT64, FIELD_CAL_RECUR_INTERVAL), DefFld(GV_INT64, FIELD_CAL_RECUR_END_COUNT), DefFld(GV_INT64, FIELD_CAL_RECUR_END_TYPE), DefFld(GV_INT64, FIELD_CAL_RECUR_FILTER_DAYS), DefFld(GV_INT64, FIELD_CAL_RECUR_FILTER_MONTHS), DefFld(GV_INT64, FIELD_CAL_PRIVACY), DefFld(GV_INT64, FIELD_COLOUR), DefFld(GV_INT64, FIELD_CAL_ALL_DAY), DefFld(GV_INT64, FIELD_STATUS), DefFld(GV_STRING, FIELD_TO), DefFld(GV_STRING, FIELD_ATTENDEE_JSON), DefFld(GV_STRING, FIELD_CAL_TIMEZONE), DefFld(GV_STRING, FIELD_CAL_SUBJECT), DefFld(GV_STRING, FIELD_CAL_LOCATION), DefFld(GV_STRING, FIELD_UID), DefFld(GV_STRING, FIELD_CAL_REMINDERS), DefFld(GV_STRING, FIELD_CAL_RECUR_FILTER_POS), DefFld(GV_STRING, FIELD_CAL_RECUR_FILTER_YEARS), DefFld(GV_STRING, FIELD_CAL_NOTES), DefFld(GV_DATETIME, FIELD_CAL_START_UTC), DefFld(GV_DATETIME, FIELD_CAL_END_UTC), DefFld(GV_DATETIME, FIELD_CAL_RECUR_END_DATE), DefFld(GV_DATETIME, FIELD_CAL_LAST_CHECK), }; class InspectTreeItem : public LTreeItem { public: LDataI *Thing; LDataPropI *Seg; class ObjectInspector *Parent; InspectTreeItem(ObjectInspector *parent, LDataI *thing); InspectTreeItem(ObjectInspector *parent, LDataPropI *seg); void Select(bool b); }; class InspectListItem : public LListItem { LAutoStreamI Stream; LStreamI *GetStream() { if (!Stream && Parent->Seg) { LDataI *di = dynamic_cast(Parent->Seg); if (di) Stream = di->GetStream(_FL); } return Stream; } public: InspectTreeItem *Parent; Field Fld; InspectListItem(InspectTreeItem *parent, Field f) { Parent = parent; Fld = f; } void Select(bool b); const char *GetText(int Col); }; ObjectInspector::ObjectInspector(LViewI *Parent, Thing *obj) : Box(NULL) { m = obj; Name("Object Inspector"); LRect r(0, 0, 1400, 700); SetPos(r); MoveToCenter(); AddView(Box = new LBox(79)); Box->AddView(Tree = new LTree(80, 0, 0, 200, 200)); Box->AddView(Lst = new LList(82, 0, 0, 200, 200)); Lst->AddColumn("Type", 65); Lst->AddColumn("Field", 165); Lst->AddColumn("Size", 60); Box->AddView(Txt = new LTextLog(81)); Box->SetSize(0, LCss::Len(LCss::LenPx, 200)); Box->SetSize(1, LCss::Len(LCss::LenPx, 300)); if (Attach(0)) { AttachChildren(); Visible(true); InspectTreeItem *Root = new InspectTreeItem(this, m->GetObject()); if (Root) { Tree->Insert(Root); Root->Select(true); } } } void ObjectInspector::OnPosChange() { if (Box) Box->SetPos(GetClient()); } InspectTreeItem::InspectTreeItem(ObjectInspector *parent, LDataI *thing) { Parent = parent; Thing = thing; Seg = NULL; switch (Thing->Type()) { case MAGIC_MAIL: { SetText("Mail"); LDataPropI *Root = Thing->GetObj(FIELD_MIME_SEG); if (Root) { Insert(new InspectTreeItem(Parent, Root)); Expanded(true); } break; } case MAGIC_CALENDAR: { SetText("Calendar"); break; } default: { LAssert(!"Impl me."); break; } } } InspectTreeItem::InspectTreeItem(ObjectInspector *parent, LDataPropI *seg) { Parent = parent; Seg = seg; Thing = NULL; auto MimeType = Seg->GetStr(FIELD_MIME_TYPE); LString s; s.Printf("Mime: %s", MimeType); SetText(s); - GDataIt Ch = Seg->GetList(FIELD_MIME_SEG); + LDataIt Ch = Seg->GetList(FIELD_MIME_SEG); if (Ch) { for (LDataPropI *c = Ch->First(); c; c = Ch->Next()) { Insert(new InspectTreeItem(Parent, c)); } Expanded(true); } } void InspectTreeItem::Select(bool b) { LTreeItem::Select(b); if (!b) return; Parent->Lst->Empty(); if (Thing) { switch (Thing->Type()) { case MAGIC_MAIL: for (unsigned i=0; iLst->Insert(new InspectListItem(this, MailProps[i])); break; case MAGIC_CALENDAR: for (unsigned i=0; iLst->Insert(new InspectListItem(this, CalendarProps[i])); break; default: LAssert(!"Impl me."); break; } } else if (Seg) { for (unsigned i=0; iLst->Insert(new InspectListItem(this, MimeSegProps[i])); } } void InspectListItem::Select(bool b) { LListItem::Select(b); if (!b) return; LTextLog *Txt = Parent->Parent->Txt; LDataPropI *Obj = Parent->Thing ? Parent->Thing : Parent->Seg; LAssert(Obj != NULL); switch (Fld.Type) { case GV_INT64: { int64 Val = Obj->GetInt(Fld.Id); LString s; s.Printf("Decimal: " LPrintfInt64 "\nHex: 0x" LPrintfHex64 "\n", Val, Val); Txt->Name(s); break; } case GV_STRING: { Txt->Name(Obj->GetStr(Fld.Id)); break; } case GV_DATETIME: { auto dt = Obj->GetDate(Fld.Id); if (dt) { char s[64] = "#error"; dt->Get(s, sizeof(s)); Txt->Name(s); } else Txt->Name("#error: NULL datetime."); break; } case GV_STREAM: { LStreamI *s = GetStream(); if (s) { int64 Sz = s->GetSize(); if (Sz > 0) { LAutoPtr Buf(new char[(int)Sz+1]); if (Buf) { s->SetPos(0); ssize_t r = s->Read(Buf, (int)Sz); if (r >= 0) { Buf[r] = 0; if (LIsUtf8(Buf)) Txt->Name(Buf); else Txt->Name("Error: Invalid utf-8"); } else Txt->Name("#error: stream read failed."); } else Txt->Name("#error: memory alloc failed."); } else Txt->Name("#error: empty stream."); } else Txt->Name("#error: NULL stream."); break; } default: { Txt->Name("#error: unknown type."); break; } } } const char *InspectListItem::GetText(int Col) { switch (Col) { case 0: switch (Fld.Type) { case GV_INT64: return "Int"; case GV_STRING: return "String"; case GV_DATETIME: return "DateTime"; case GV_STREAM: return "Stream"; default: return "#error: unknown type"; } break; case 1: return Fld.Name; case 2: { static char s[16]; switch (Fld.Type) { case GV_INT64: return (char*)"8"; case GV_STRING: { LDataPropI *Obj = Parent->Thing ? Parent->Thing : Parent->Seg; auto Str = Obj->GetStr(Fld.Id); if (!Str) return (char*)"NULL"; sprintf_s(s, sizeof(s), "%i", (int)strlen(Str)); return s; } case GV_DATETIME: { sprintf_s(s, sizeof(s), "%i", (int)sizeof(LDateTime)); return s; } case GV_STREAM: { LStreamI *Str = GetStream(); if (Str) { sprintf_s(s, sizeof(s), LPrintfInt64, Str->GetSize()); return s; } break; } default: break; } break; } } return NULL; } diff --git a/Code/Scribe.h b/Code/Scribe.h --- a/Code/Scribe.h +++ b/Code/Scribe.h @@ -1,2563 +1,2564 @@ /*hdr ** FILE: Scribe.h ** AUTHOR: Matthew Allen ** DATE: 22/10/97 ** DESCRIPTION: Scribe email application ** ** Copyright (C) 1998-2003 Matthew Allen ** fret@memecode.com */ // Includes #include #include #include "lgi/common/Lgi.h" #include "lgi/common/DragAndDrop.h" #include "lgi/common/DateTime.h" #include "lgi/common/Password.h" #include "lgi/common/vCard-vCal.h" #include "lgi/common/WordStore.h" #include "lgi/common/SharedMemory.h" #include "lgi/common/XmlTreeUi.h" #include "lgi/common/Mime.h" #include "lgi/common/OptionsFile.h" #include "lgi/common/TextLog.h" #include "lgi/common/Menu.h" #include "lgi/common/ToolBar.h" #include "lgi/common/Combo.h" #include "lgi/common/Printer.h" // Gui controls #include "lgi/common/Panel.h" #include "lgi/common/DocView.h" #include "lgi/common/List.h" #include "lgi/common/Tree.h" #include "lgi/common/ListItemCheckBox.h" // Storage #include "lgi/common/Store3.h" // App Includes #include "ScribeInc.h" #include "ScribeUtils.h" #include "ScribeDefs.h" #include "DomType.h" class ListAddr; // The field definition type struct ItemFieldDef { const char *DisplayText; ScribeDomType Dom; LVariantType Type; int FieldId; // Was 'Id' int CtrlId; const char *Option; bool UtcConvert; }; //////////////////////////////////////////////////////////////////////////////////////////// // Classes class MailTree; class LMailStore; class ScribeWnd; class Thing; class Mail; class Contact; class ThingUi; class MailUi; class ScribeFolder; class ContactUi; class FolderPropertiesDlg; class ScribeAccount; class Filter; class Attachment; class Calendar; class CalendarSource; class AttachmentList; struct ItemFieldDef; class FolderDlg; class Filter; class ContactGroup; class ScribeBehaviour; class AccountletThread; class ThingList; class LSpellCheck; //////////////////////////////////////////////////////////////////////// // Scripting support #include "lgi/common/Scripting.h" /// Script callback types. See 'api.html' in the Scripts folder for more details. enum LScriptCallbackType { LCallbackNull, LToolsMenu, LThingContextMenu, LThingUiToolbar, LApplicationToolbar, LMailOnBeforeSend, // "OnBeforeMailSend" LMailOnAfterReceive, LBeforeInstallBar, LInstallComponent, LFolderContextMenu, LOnTimer, LRenderMail, LOnLoad }; struct LScript; struct LScriptCallback { LScriptCallbackType Type = LCallbackNull; LScript *Script = NULL; LFunctionInfo *Func = NULL; int Param = 0; double fParam = 0.0; LVariant Data; uint64 PrevTs = 0; bool OnSecond = false; }; struct LScript { LAutoPtr Code; LArray Callbacks; }; typedef void (*ConsoleClosingCallback)(class LScriptConsole *Console, void *user_data); class LScriptConsole : public LWindow { ScribeWnd *App; LTextLog *Txt; ConsoleClosingCallback Callback; void *CallbackData; bool OnViewKey(LView *v, LKey &k); public: LScriptConsole(ScribeWnd *app, ConsoleClosingCallback callback, void *callback_data); ~LScriptConsole(); void Write(const char *s, int64 Len); bool OnRequestClose(bool OsShuttingDown); }; /// This class is a wrapper around a user interface element used for /// Scripting. The script engine needs to be able to store information /// pertaining to the menu item's callbacks along with the sub menu. class LScriptUi : public LDom { public: LScriptUi *Parent; LSubMenu *Sub; LToolBar *Toolbar; LArray Callbacks; LArray Subs; LScriptUi() { Parent = 0; Sub = 0; Toolbar = 0; } LScriptUi(LSubMenu *s) { Parent = 0; Toolbar = 0; Sub = s; } LScriptUi(LToolBar *t) { Parent = 0; Toolbar = t; Sub = 0; } ~LScriptUi() { Subs.DeleteObjects(); } bool GetVariant(const char *Name, LVariant &Value, const char *Arr = NULL) override { if (Sub) return Sub->GetVariant(Name, Value, Arr); LDomProperty Method = LStringToDomProp(Name); if (Method == ObjLength) { if (Toolbar) Value = (int64)Toolbar->Length(); } else return false; return true; } bool CallMethod(const char *MethodName, LVariant *ReturnValue, LArray &Args) override { if (Sub) return Sub->CallMethod(MethodName, ReturnValue, Args); return false; } bool SetupCallbacks(ScribeWnd *App, ThingUi *Parent, Thing *t, LScriptCallbackType Type); bool ExecuteCallbacks(ScribeWnd *App, ThingUi *Parent, Thing *t, int Cmd); }; class LScribeScript : public LScriptContext { LScriptEngine *Eng; struct LScribeScriptPriv *d; public: ScribeWnd *App; static LScribeScript *Inst; LScribeScript(ScribeWnd *app); ~LScribeScript(); void ShowScriptingWindow(bool show); LAutoString GetDataFolder(); LStream *GetLog(); GHostFunc *GetCommands(); char *GetIncludeFile(char *FileName); // System void SetEngine(LScriptEngine *eng); bool MsgBox(LScriptArguments &Args); // Paths bool GetSystemPath(LScriptArguments &Args); bool GetScribeTempPath(LScriptArguments &Args); bool JoinPath(LScriptArguments &Args); // Folders bool GetFolder(LScriptArguments &Args); bool GetSourceFolders(LScriptArguments &Args); bool CreateSubFolder(LScriptArguments &Args); bool LoadFolder(LScriptArguments &Args); bool FolderSelect(LScriptArguments &Args); bool BrowseFolder(LScriptArguments &Args); // Things bool CreateThing(LScriptArguments &Args); bool MoveThing(LScriptArguments &Args); bool SaveThing(LScriptArguments &Args); bool DeleteThing(LScriptArguments &Args); bool ShowThingWindow(LScriptArguments &Args); bool FilterDoActions(LScriptArguments &Args); bool LookupContact(LScriptArguments &Args); // Callbacks bool AddToolsMenuItem(LScriptArguments &Args); bool AddCallback(LScriptArguments &Args); // UI bool MenuAddItem(LScriptArguments &Args); bool MenuAddSubmenu(LScriptArguments &Args); bool ToolbarAddItem(LScriptArguments &Args); }; //////////////////////////////////////////////////////////////////////// class ScribePassword { class ScribePasswordPrivate *d; public: ScribePassword(LOptionsFile *p, const char *opt, int check, int pwd, int confirm); ~ScribePassword(); bool IsOk(); bool Load(LView *dlg); bool Save(); void OnNotify(LViewI *c, LNotification &n); }; class ChooseFolderDlg : public LDialog { ScribeWnd *App; LEdit *Folder; int Type; bool Export; LList *Lst; void InsertFile(const char *f); public: LString DestFolder; LString::Array SrcFiles; ChooseFolderDlg ( ScribeWnd *parent, bool IsExport, const char *Title, const char *Msg, char *DefFolder = NULL, int FolderType = MAGIC_MAIL, LString::Array *Files = NULL ); int OnNotify(LViewI *Ctrl, LNotification n); }; #define IoProgressImplArgs LAutoPtr stream, const char *mimeType, IoProgressCallback cb #define IoProgressFnArgs IoProgressImplArgs = NULL #define IoProgressError(err) \ { \ IoProgress p(Store3Error, err); \ if (cb) cb(&p, stream); \ return p; \ } #define IoProgressSuccess() \ { \ IoProgress p(Store3Success); \ if (cb) cb(&p, stream); \ return p; \ } #define IoProgressNotImpl() \ { \ IoProgress p(Store3NotImpl); \ if (cb) cb(&p, stream); \ return p; \ } class ScribeClass ThingType : public LDom, public LDataUserI { bool Dirty = false; bool WillDirty = true; bool Loaded = false; protected: bool GetDirty() { return Dirty; } bool OnError(const char *File, int Line) { _lgi_assert(false, "Object Missing", File, Line); return false; } // Callbacks struct ThingEventInfo { const char *File = NULL; int Line = 0; std::function Callback; }; LArray OnLoadCallbacks; public: struct IoProgress; typedef std::function IoProgressCallback; struct IoProgress { // This is the main result to look at: // Store3NotImpl - typically means the mime type is wrong. // Store3Error - an error occured. // Store3Delayed - means the operation will take a long time. // However progress is report via 'prog' if not NULL. // And the 'onComplete' handler will be called at the end. // Store3Success - the operation successfully completed. Store3Status status = Store3NotImpl; // Optional progress for the operation. Really only relevant for // status == Store3Delayed. Progress *prog = NULL; // Optional error message for the operation. Relevant if // status == Store3Error. LString errMsg; IoProgress(Store3Status s, const char *err = NULL) { status = s; if (err) errMsg = err; } operator bool() { return status > Store3Error; } }; template LAutoPtr AutoCast(LAutoPtr ap) { return LAutoPtr(ap.Release()); } static LArray DirtyThings; ScribeWnd *App = NULL; ThingType(); virtual ~ThingType(); virtual Store3ItemTypes Type() { return MAGIC_NONE; } virtual bool SetDirty(bool b = true); void SetWillDirty(bool c) { WillDirty = c; } virtual bool Save(ScribeFolder *Into) { return false; } virtual void OnProperties(int Tab = -1) {} virtual ScribeFolder *GetFolder() = 0; virtual Store3Status SetFolder(ScribeFolder *f, int Param = -1) = 0; virtual bool IsPlaceHolder() { return false; } // Events void WhenLoaded(const char *file, int line, std::function Callback, int index = -1); bool IsLoaded(int Set = -1); // Printing virtual void OnPrintHeaders(struct ScribePrintContext &Context) { LAssert(!"Impl me."); } virtual void OnPrintText(ScribePrintContext &Context, LPrintPageRanges &Pages) { LAssert(!"Impl me."); } virtual int OnPrintHtml(ScribePrintContext &Context, LPrintPageRanges &Pages, LSurface *RenderedHtml) { LAssert(!"Impl me."); return 0; } }; class MailContainerIter; class ScribeClass MailContainer { friend class MailContainerIter; List Iters; public: virtual ~MailContainer(); virtual size_t Length() { return 0; } virtual ssize_t IndexOf(Mail *m) { return -1; } virtual Mail *operator [](size_t i) { return NULL; } }; class ScribeClass MailContainerIter { friend class MailContainer; protected: MailContainer *Container; public: MailContainerIter(); ~MailContainerIter(); void SetContainer(MailContainer *c); }; class ScribeClass ThingStorage // External storage information { public: int Data; ThingStorage() { Data = 0; } virtual ~ThingStorage() {} }; class ThingUi; class ScribeClass Thing : public ThingType, public LListItem, public LDragDropSource, public LRefCount { friend class ScribeWnd; friend class ScribeFolder; ScribeFolder *_ParentFolder = NULL; protected: LArray FieldArray; LAutoString DropFileName; // This structure allows the app to move objects between // mail stores. After a delayed write to the new mail store // the old item needs to be removed. This keeps track of // where that old item is. Don't assume that the Obj pointer // is valid... check in the folder's items first. struct ThingReference { LString Path; Thing *Obj; ThingReference() { Obj = NULL; } } DeleteOnAdd; public: ThingStorage *Data = NULL; Thing(ScribeWnd *app, LDataI *object = 0); ~Thing(); // Dom bool CallMethod(const char *MethodName, LVariant *ReturnValue, LArray &Args) override; // D'n'd bool GetData(LArray &Data) override; bool GetFormats(LDragFormats &Formats) override; bool OnBeginDrag(LMouse &m) override; // Import / Export virtual bool GetFormats(bool Export, LString::Array &MimeTypes) { return false; } // Anything implementing 2 functions should mostly be using one of these // to "return" an IoProgress and process any callback: // IoProgressError(msg) // IoProgressNotImpl() // IoProgressSuccess() virtual IoProgress Import(IoProgressFnArgs) = 0; virtual IoProgress Export(IoProgressFnArgs) = 0; /// This exports all the selected items void ExportAll(LViewI *Parent, const char *ExportMimeType, std::function Callback); // UI bool OnKey(LKey &k) override; // Thing ScribeFolder *GetFolder() override { return _ParentFolder; } void SetParentFolder(ScribeFolder *f); Store3Status SetFolder(ScribeFolder *f, int Param = -1) override; LDataI *DefaultObject(LDataI *arg = 0); virtual ThingUi *DoUI(MailContainer *c = 0) { return NULL; } virtual ThingUi *GetUI() { return 0; } virtual bool SetUI(ThingUi *ui = 0) { return false; } virtual uint32_t GetFlags() { return 0; } virtual void OnCreate() override; virtual bool OnDelete(); virtual int *GetDefaultFields() { return 0; } virtual const char *GetFieldText(int Field) { return 0; } virtual void DoContextMenu(LMouse &m, LView *Parent = 0) {} virtual Thing &operator =(Thing &c) { LAssert(0); return *this; } virtual char *GetDropFileName() = 0; virtual bool GetDropFiles(LString::Array &Files) { return false; } virtual void OnSerialize(bool Write) {} // Interfaces virtual Mail *IsMail() { return 0; } virtual Contact *IsContact() { return 0; } virtual ContactGroup *IsGroup() { return 0; } virtual Filter *IsFilter() { return 0; } virtual Attachment *IsAttachment() { return 0; } virtual Calendar *IsCalendar() { return 0; } void SetFieldArray(LArray &i) { FieldArray = i; } void OnMove(); bool SetField(int Field, int n); bool SetField(int Field, double n); bool SetField(int Field, char *n); bool SetField(int Field, LDateTime &n); bool GetField(int Field, int &n); bool GetField(int Field, double &n); bool GetField(int Field, const char *&n); bool GetField(int Field, LDateTime &n); bool DeleteField(int Field); }; class ThingUi : public LWindow { friend class MailUiGpg; bool _Dirty; char *_Name; protected: Thing *_Item; bool _Running; void SetItem(Thing *i) { _Item = i; } public: ScribeWnd *App; static LArray All; ThingUi(Thing *item, const char *name); ~ThingUi(); virtual bool SetDirty(bool d, bool ui = true); bool IsDirty() { return _Dirty; } bool OnRequestClose(bool OsShuttingDown); bool OnViewKey(LView *v, LKey &k); virtual void OnDirty(bool Dirty) {} virtual void OnLoad() = 0; virtual void OnSave() = 0; virtual void OnChange() {} virtual AttachmentList *GetAttachments() { return 0; } virtual bool AddRecipient(AddressDescriptor *Addr) { return false; } }; class ThingFilter { public: virtual bool TestThing(Thing *Thing) = 0; }; class ScribeClass Attachment : public Thing { friend class Mail; protected: Mail *Msg; Mail *Owner; bool IsResizing; LString Buf; // D'n'd LAutoString DropSourceFile; bool GetFormats(LDragFormats &Formats) override; bool GetData(LArray &Data) override; void _New(LDataI *object); public: enum Encoding { OCTET_STREAM, PLAIN_TEXT, BASE64, QUOTED_PRINTABLE, }; Attachment(ScribeWnd *App, Attachment *import = 0); Attachment(ScribeWnd *App, LDataI *object, const char *import = 0); ~Attachment(); bool ImportFile(const char *FileName); bool ImportStream(const char *FileName, const char *MimeType, LAutoStreamI Stream); Thing &operator =(Thing &c) override; LDATA_INT64_PROP(Size, FIELD_SIZE); LDATA_STR_PROP(Name, FIELD_NAME); LDATA_STR_PROP(MimeType, FIELD_MIME_TYPE); LDATA_STR_PROP(ContentId, FIELD_CONTENT_ID); LDATA_STR_PROP(Charset, FIELD_CHARSET); LDATA_STR_PROP(InternetHeaders, FIELD_INTERNET_HEADER); // LDom support bool GetVariant(const char *Name, LVariant &Value, const char *Array = NULL) override; bool CallMethod(const char *MethodName, LVariant *Ret, LArray &Args) override; void OnOpen(LView *Parent, char *Dest = 0); void OnDeleteAttachment(LView *Parent, bool Ask); void OnSaveAs(LView *Parent); void OnMouseClick(LMouse &m) override; bool OnKey(LKey &k) override; Store3ItemTypes Type() override { return MAGIC_ATTACHMENT; } bool Get(char **ptr, ssize_t *size); bool Set(char *ptr, ssize_t size); bool Set(LAutoStreamI Stream); Attachment *IsAttachment() override { return this; } LAutoString MakeFileName(); bool GetIsResizing(); void SetIsResizing(bool b); bool IsMailMessage(); bool IsVCalendar(); bool IsVCard(); // The owner is the mail that this is attached to Mail *GetOwner() { return Owner; } void SetOwner(Mail *msg); // The msg is the mail that this message/rfc822 attachment is rendered into Mail *GetMsg(); void SetMsg(Mail *m); LStreamI *GotoObject(const char *file, int line); int Sizeof(); bool Serialize(LFile &f, bool Write); IoProgress Import(IoProgressFnArgs) override { return Store3Error; } IoProgress Export(IoProgressFnArgs) override { return Store3Error; } bool SaveTo(char *FileName, bool Quite = false, LView *Parent = 0); const char *GetText(int i) override; char *GetDropFileName() override; bool GetDropFiles(LString::Array &Files) override; }; class ScribeClass Contact : public Thing { friend class ContactUi; protected: class ContactPriv *d = NULL; ContactUi *Ui = NULL; public: static List Everyone; static Contact *LookupEmail(const char *Email); static LHashTbl, int> PropMap; static int DefaultContactFields[]; Contact(ScribeWnd *app, LDataI *object = 0); ~Contact(); LDATA_STR_PROP(First, FIELD_FIRST_NAME); LDATA_STR_PROP(Last, FIELD_LAST_NAME); LDATA_STR_PROP(Email, FIELD_EMAIL); bool Get(const char *Opt, const char *&Value); bool Set(const char *Opt, const char *Value); bool Get(const char *Opt, int &Value); bool Set(const char *Opt, int Value); // operators Thing &operator =(Thing &c) override; Contact *IsContact() override { return this; } // Dom bool GetVariant(const char *Name, LVariant &Value, const char *Array = 0) override; bool SetVariant(const char *Name, LVariant &Value, const char *Array = 0) override; bool CallMethod(const char *MethodName, LVariant *ReturnValue, LArray &Args) override; // Events void OnMouseClick(LMouse &m) override; // Printing void OnPrintHeaders(struct ScribePrintContext &Context) override; void OnPrintText(ScribePrintContext &Context, LPrintPageRanges &Pages) override; // Misc Store3ItemTypes Type() override { return MAGIC_CONTACT; } ThingUi *DoUI(MailContainer *c = 0) override; int Compare(LListItem *Arg, ssize_t Field) override; bool IsAssociatedWith(char *PluginName); char *GetLocalTime(const char *TimeZone = 0); // Email address int GetAddrCount(); LString::Array GetEmails(); LString GetAddrAt(int i); bool HasEmail(LString email); // Serialization size_t SizeofField(const char *Name); size_t Sizeof(); bool Serialize(LFile &f, bool Write); bool Save(ScribeFolder *Into = 0) override; // ListItem const char *GetText(int i) override; int *GetDefaultFields() override; const char *GetFieldText(int Field) override; int GetImage(int Flags = 0) override { return ICON_CONTACT; } // Import/Export bool GetFormats(bool Export, LString::Array &MimeTypes) override; IoProgress Import(IoProgressFnArgs) override; IoProgress Export(IoProgressFnArgs) override; char *GetDropFileName() override; bool GetDropFiles(LString::Array &Files) override; }; #define ContactGroupObj "ContactGroup" #define ContactGroupName "Name" #define ContactGroupList "List" #define ContactGroupDateModified "DateModified" extern ItemFieldDef GroupFieldDefs[]; class ContactGroup : public Thing { friend class GroupUi; class GroupUi *Ui; class ContactGroupPrivate *d; LString DateCache; public: LDateTime UsedTs; LDATA_STR_PROP(Name, FIELD_GROUP_NAME); ContactGroup(ScribeWnd *app, LDataI *object = 0); ~ContactGroup(); // operators Thing &operator =(Thing &c) override; ContactGroup *IsGroup() override { return this; } // Dom bool GetVariant(const char *Name, LVariant &Value, const char *Array = NULL) override; bool CallMethod(const char *MethodName, LVariant *ReturnValue, LArray &Args) override; // Events void OnMouseClick(LMouse &m) override; void OnSerialize(bool Write) override; // Misc Store3ItemTypes Type() override { return MAGIC_GROUP; } ThingUi *DoUI(MailContainer *c = 0) override; int Compare(LListItem *Arg, ssize_t Field) override; bool GetAddresses(List &a); LString::Array GetAddresses(); // Serialization bool Save(ScribeFolder *Into = 0) override; // ListItem const char *GetText(int i) override; int *GetDefaultFields() override; const char *GetFieldText(int Field) override; int GetImage(int Flags = 0) override { return ICON_CONTACT_GROUP; } // Import / Export char *GetDropFileName() override; bool GetDropFiles(LString::Array &Files) override; bool GetFormats(bool Export, LString::Array &MimeTypes) override; IoProgress Import(IoProgressFnArgs) override; IoProgress Export(IoProgressFnArgs) override; }; struct LGroupMapArray : public LArray { LString toString() { LString::Array a; for (auto i: *this) a.Add(i->GetName()); return LString(",").Join(a); } }; class LGroupMap : public LHashTbl,LGroupMapArray*> { ScribeWnd *App; void Index(ContactGroup *grp); public: LGroupMap(ScribeWnd *app); ~LGroupMap(); }; ///////////////////////////////////////////////////////////// // Mail threading // // See: http://www.jwz.org/doc/threading.html struct ThingSortParams { int SortAscend; int SortField; }; class MContainer { int Lines; int Depth; bool Open; bool Next; void Dump(LStream &s, int Depth = 0); public: typedef LHashTbl, MContainer*> ContainerHash; // Container data Mail *Message; MContainer *Parent; LArray Children; List Refs; int Index; // Cache data List RefCache; // Debug #ifdef _DEBUG LAutoString MsgId; #endif // Methods MContainer(const char *Id, Mail *m = 0); ~MContainer(); void SetMail(Mail *m); Mail *GetTop(); bool HasChild(MContainer *m); void AddChild(MContainer *m); void RemoveChild(MContainer *m); int CountMessages(); void Pour(int &index, int depth, int tree, bool next, ThingSortParams *folder); void OnPaint(LSurface *pDC, LRect &r, LItemColumn *c, LColour Fore, LColour Back, LFont *Font, const char *Txt); static void Prune(int &ParentIndex, LArray &L); static void Thread(List &In, LArray &Out); }; extern int ListItemCompare(LListItem *a, LListItem *b, NativeInt Data); extern int ContainerIndexer(Thing *a, Thing *b, NativeInt Data); extern const char *Store3ItemTypeName(Store3ItemTypes t); extern int GetFolderVersion(const char *Path); extern bool CreateMailHeaders(ScribeWnd *App, LStream &Out, LDataI *Mail, MailProtocol *Protocol); //////////////////////////////////////////////////////////// // Thing sorting // The old way extern int ContainerCompare(MContainer **a, MContainer **b); extern int ThingCompare(Thing *a, Thing *b, NativeInt Data); // The new way extern int ContainerSorter(MContainer *&a, MContainer *&b, ThingSortParams *Params); extern int ThingSorter(Thing *a, Thing *b, ThingSortParams *Data); ///////////////////////////////////////////////////////////// class MailViewOwner : public LCapabilityTarget { public: virtual LDocView *GetDoc(const char *MimeType) = 0; virtual bool SetDoc(LDocView *v, const char *MimeType) = 0; }; ///////////////////////////////////////////////////////////// // The core mail object struct MailPrintContext; class ScribeClass Mail : public Thing, public MailContainer, public LDefaultDocumentEnv { friend class MailUi; friend class MailPropDlg; friend class ScribeFolder; friend class Attachment; friend class ScribeWnd; private: class MailPrivate *d; static LHashTbl,Mail*> MessageIdMap; // List item preview int PreviewCacheX; List PreviewCache; int64_t TotalSizeCache; int64_t FlagsCache; MailUi *Ui; int Cursor; // Stores the cursor position in reply/forward format until the UI needs it Attachment *ParentFile; List Attachments; Mail *PreviousMail; // the mail we are replying to / forwarding void _New(); void _Delete(); bool _GetListItems(List &l, bool All); // All=false is just the selected items void SetListRead(bool Read); void SetFlagsCache(int64_t NewFlags, bool IgnoreReceipt, bool UpdateScreen); // LDocumentEnv impl List Actions; bool OnNavigate(LDocView *Parent, const char *Uri) override; bool AppendItems(LSubMenu *Menu, const char *Param, int Base = 1000) override; bool OnMenu(LDocView *View, int Id, void *Context) override; LoadType GetContent(LoadJob *&j) override; public: static bool PreviewLines; static bool AdjustDateTz; static int DefaultMailFields[]; static bool RunMailPipes; static List NewMailLst; constexpr static float MarkColourMix = 0.9f; uint8_t SendAttempts; enum NewEmailState { NewEmailNone, NewEmailLoading, NewEmailFilter, NewEmailBayes, NewEmailGrowl, NewEmailTray }; NewEmailState NewEmail; Mail(ScribeWnd *app, LDataI *object = 0); ~Mail(); bool SetObject(LDataI *o, bool InDataDestuctor, const char *File, int Line) override; LDATA_STR_PROP(Label, FIELD_LABEL); LDATA_STR_PROP(FwdMsgId, FIELD_FWD_MSG_ID); LDATA_STR_PROP(BounceMsgId, FIELD_BOUNCE_MSG_ID); LDATA_STR_PROP(Subject, FIELD_SUBJECT); LDATA_STR_PROP(Body, FIELD_TEXT); LDATA_STR_PROP(BodyCharset, FIELD_CHARSET); LDATA_STR_PROP(Html, FIELD_ALTERNATE_HTML); LDATA_STR_PROP(HtmlCharset, FIELD_HTML_CHARSET); LDATA_STR_PROP(InternetHeader, FIELD_INTERNET_HEADER); LDATA_INT_TYPE_PROP(EmailPriority, Priority, FIELD_PRIORITY, MAIL_PRIORITY_NORMAL); LDATA_INT32_PROP(AccountId, FIELD_ACCOUNT_ID); LDATA_INT64_PROP(MarkColour, FIELD_COLOUR); LDATA_STR_PROP(References, FIELD_REFERENCES); LDATA_DATE_PROP(DateReceived, FIELD_DATE_RECEIVED); LDATA_DATE_PROP(DateSent, FIELD_DATE_SENT); LDATA_INT_TYPE_PROP(Store3State, Loaded, FIELD_LOADED, Store3Loaded); LVariant GetServerUid(); bool SetServerUid(LVariant &v); const char *GetFromStr(int id) { LDataPropI *From = GetObject() ? GetObject()->GetObj(FIELD_FROM) : 0; return From ? From->GetStr(id) : NULL; } LDataPropI *GetFrom() { return GetObject() ? GetObject()->GetObj(FIELD_FROM) : 0; } LDataPropI *GetReply() { return GetObject() ? GetObject()->GetObj(FIELD_REPLY) : 0; } - GDataIt GetTo() + LDataIt GetTo() { return GetObject() ? GetObject()->GetList(FIELD_TO) : 0; } bool GetAttachmentObjs(LArray &Objs); LDataI *GetFileAttachPoint(); // Operators Mail *IsMail() override { return this; } Thing &operator =(Thing &c) override; OsView Handle(); ThingUi *GetUI() override; bool SetUI(ThingUi *ui) override; // References and ID's MContainer *Container; const char *GetMessageId(bool Create = false); bool SetMessageId(const char *val); LAutoString GetThreadIndex(int TruncateChars = 0); static Mail *GetMailFromId(const char *Id); bool MailMessageIdMap(bool Add = true); bool GetReferences(List &Ids); void GetThread(List &Thread); LString GetMailRef(); bool ResizeImage(Attachment *a); // MailContainer size_t Length() override; ssize_t IndexOf(Mail *m) override; Mail *operator [](size_t i) override; // Dom bool GetVariant(const char *Name, LVariant &Value, const char *Array = NULL) override; bool SetVariant(const char *Name, LVariant &Value, const char *Array = NULL) override; bool CallMethod(const char *MethodName, LVariant *ReturnValue, LArray &Args) override; // Events void OnCreate() override; bool OnBeforeSend(struct ScribeEnvelope *Out); void OnAfterSend(); bool OnBeforeReceive(); bool OnAfterReceive(LStreamI *Msg); void OnMouseClick(LMouse &m) override; void OnProperties(int Tab = -1) override; void OnInspect(); void OnReply(Mail *m, bool All, bool MarkOriginal); bool OnForward(Mail *m, bool MarkOriginal, int WithAttachments = -1); bool OnBounce(Mail *m, bool MarkOriginal, int WithAttachments = -1); void OnReceipt(Mail *m); int OnNotify(LViewI *Ctrl, LNotification n) override; // Printing void OnPrintHeaders(ScribePrintContext &Context) override; void OnPrintText(ScribePrintContext &Context, LPrintPageRanges &Pages) override; int OnPrintHtml(ScribePrintContext &Context, LPrintPageRanges &Pages, LSurface *RenderedHtml) override; // Misc uint32_t GetFlags() override; void SetFlags(ulong i, bool IgnoreReceipt = false, bool Update = true); void DeleteAsSpam(LView *View); const char *GetFieldText(int Field) override; LAutoString GetCharSet(); char *GetNewText(int Max = 64 << 10, const char *AsCp = "utf-8"); int *GetDefaultFields() override; Store3ItemTypes Type() override { return MAGIC_MAIL; } void DoContextMenu(LMouse &m, LView *Parent = 0) override; int Compare(LListItem *Arg, ssize_t Field) override; char *GetDropFileName() override; bool GetDropFiles(LString::Array &Files) override; LAutoString GetSig(bool HtmlVersion, ScribeAccount *Account = 0); bool LoadFromFile(char *File); void PrepSend(); void NewRecipient(char *Email, char *Name = 0); void ClearCachedItems(); bool Send(bool Now); void CreateMailHeaders(); bool AddCalendarEvent(LViewI *Parent, bool AddPopupReminder, LString *Msg); LArray GetCalendarAttachments(); // UI LDocView *CreateView(MailViewOwner *Owner, LString MimeType, bool Sunken, size_t MaxBytes, bool NoEdit = false); ThingUi *DoUI(MailContainer *c = 0) override; // Alt HTML bool HasAlternateHtml(Attachment **Attach = 0); char *GetAlternateHtml(List *Refs = 0); // dynamically allocated ptr bool WriteAlternateHtml(char *File = NULL, int FileLen = 0); // defaults to TEMP dir // Account stuff void ProcessTextForResponse(Mail *From, LOptionsFile *Options, ScribeAccount *Account); void WrapAndQuote(LStringPipe &Pipe, const char *QuoteStr, int WrapAt = -1); ScribeAccount *GetAccountSentTo(); // Access int64 TotalSizeof(); bool Save(ScribeFolder *Into = 0) override; // Attachments Attachment *AttachFile(LView *Parent, const char *FileName); bool AttachFile(Attachment *File); bool DeleteAttachment(Attachment *File); + LArray GetAttachments(); bool GetAttachments(List *Attachments); bool HasAttachments() { return Attachments.Length() > 0; } bool UnloadAttachments(); // Import / Export bool GetFormats(bool Export, LString::Array &MimeTypes) override; IoProgress Import(IoProgressFnArgs) override; IoProgress Export(IoProgressFnArgs) override; // ListItem void Update() override; const char *GetText(int i) override; int GetImage(int SelFlags = 0) override; void OnMeasure(LPoint *Info) override; void OnPaintColumn(LItem::ItemPaintCtx &Ctx, int i, LItemColumn *c) override; void OnPaint(LItem::ItemPaintCtx &Ctx) override; }; inline const char *toString(Mail::NewEmailState s) { #define _(s) case Mail::s: return #s; switch (s) { _(NewEmailNone) _(NewEmailLoading) _(NewEmailFilter) _(NewEmailBayes) _(NewEmailGrowl) _(NewEmailTray) } #undef _ LAssert(0); return "#invalidNewEmailState"; } // this is where the items reside // and it forms a leaf on the mail box tree // to the user it looks like a folder class ScribeClass ScribeFolder : public ThingType, public LTreeItem, public LDragDropSource, public MailContainer { friend class MailTree; friend class FolderPropertiesDlg; friend class ThingList; friend class ScribeWnd; protected: class ScribeFolderPriv *d; ThingList *View(); LString DropFileName; LString GetDropFileName(); // UI cache LAutoString NameCache; int ChildUnRead = 0; LTreeItem *LoadOnDemand = NULL; LAutoPtr Loading; LArray FieldArray; void SerializeFieldWidths(bool Write = false); void EmptyFieldList(); void SetLoadFolder(Thing *t) { if (t) t->SetParentFolder(this); } bool HasFieldId(int Id); // Tree item stuff void _PourText(LPoint &Size) override; void _PaintText(LItem::ItemPaintCtx &Ctx) override; int _UnreadChildren(); void UpdateOsUnread(); // Debugging state enum FolderState { FldState_Idle, FldState_Loading, FldState_Populating, } CurState = FldState_Idle; public: List Items; ScribeFolder(); ~ScribeFolder(); // Object LArray &GetFieldArray() { return FieldArray; } LDataFolderI *GetFldObj() { return dynamic_cast(GetObject()); } bool SetObject(LDataI *o, bool InDataDestuctor, const char *File, int Line) override; // ThingType Store3ItemTypes Type() override { return GetObject() ? (Store3ItemTypes)GetObject()->Type() : MAGIC_NONE; } ScribeFolder *GetFolder() override { return dynamic_cast(LTreeItem::GetParent()); } ScribeFolder *GetChildFolder() { return dynamic_cast(LTreeItem::GetChild()); } ScribeFolder *GetNextFolder() { return dynamic_cast(LTreeItem::GetNext()); } Store3Status SetFolder(ScribeFolder *f, int Param = -1) override; ScribeFolder *IsFolder() { return this; } Store3Status CopyTo(ScribeFolder *NewParent, int NewIndex = -1); // MailContainer size_t Length() override; ssize_t IndexOf(Mail *m) override; Mail *operator [](size_t i) override; /// Update the unread count void OnUpdateUnRead ( /// Increments the count, or zero if a child folder is changing. int Offset, /// Re-scan the folder bool ScanItems ); // Methods LDATA_INT32_PROP(UnRead, FIELD_UNREAD); LDATA_INT_TYPE_PROP(Store3ItemTypes, ItemType, FIELD_FOLDER_TYPE, MAGIC_MAIL); LDATA_INT32_PROP(Open, FIELD_FOLDER_OPEN); LDATA_INT32_PROP(SortIndex, FIELD_FOLDER_INDEX); LDATA_INT64_PROP(Items, FIELD_FOLDER_ITEMS); // Cached item count LDATA_INT_TYPE_PROP(ScribePerm, ReadAccess, FIELD_FOLDER_PERM_READ, PermRequireNone); LDATA_INT_TYPE_PROP(ScribePerm, WriteAccess, FIELD_FOLDER_PERM_WRITE, PermRequireNone); LDATA_ENUM_PROP(SystemFolderType, FIELD_SYSTEM_FOLDER, Store3SystemFolder); void SetSort(int Col, bool Ascend, bool CanDirty = true); int GetSortAscend() { return GetObject()->GetInt(FIELD_SORT) > 0; } int GetSortCol() { return abs((int)GetObject()->GetInt(FIELD_SORT)) - 1; } int GetSortField(); void ReSort(); bool Save(ScribeFolder *Into = 0) override; bool ReindexField(int OldIndex, int NewIndex); void CollectSubFolderMail(ScribeFolder *To = 0); bool InsertThing(Thing *Item); bool MoveTo(LArray &Items, bool CopyOnly = false, LArray *Status = NULL); bool Delete(LArray &Items, bool ToTrash); void SetDefaultFields(bool Force = false); bool Thread(); ScribePerm GetFolderPerms(ScribeAccessType Access); void SetFolderPerms(LView *Parent, ScribeAccessType Access, ScribePerm Perm, std::function Callback); bool GetThreaded(); void SetThreaded(bool t); // void Update(); void GetMessageById(const char *Id, std::function Callback); void SetLoadOnDemand(); void SortSubfolders(); void DoContextMenu(LMouse &m); void OnItemType(); bool IsInTrash(); bool SortItems(); // Virtuals: /// /// These methods can be used in a synchronous or asynchronous manner: /// sync: Call with 'Callback=NULL' and use the return value. /// If the function needs to show a dialog (like to get permissions from /// the user) then it'll return Store3Delayed immediately. /// async: Call with a valid callback, and the method will possibly wait /// for the user and then either return Store3Error or Store3Success. virtual Store3Status LoadThings(LViewI *Parent = NULL, std::function Callback = NULL); virtual Store3Status WriteThing(Thing *t, std::function Callback = NULL); virtual Store3Status DeleteThing(Thing *t, std::function Callback = NULL); virtual Store3Status DeleteAllThings( std::function Callback = NULL); virtual bool LoadFolders(); virtual bool UnloadThings(); virtual bool IsWriteable() { return true; } virtual bool IsPublicFolders() { return false; } virtual void OnProperties(int Tab = -1) override; virtual ScribeFolder *CreateSubDirectory(const char *Name, int Type); virtual void OnRename(char *NewName); virtual void OnDelete(); virtual LString GetPath(); virtual ScribeFolder *GetSubFolder(const char *Path); virtual void Populate(ThingList *List); virtual bool CanHaveSubFolders(Store3ItemTypes Type = MAGIC_MAIL) { return GetItemType() != MAGIC_ANY; } virtual void OnRethread(); // Name void SetName(const char *Name, bool Encode); LString GetName(bool Decode); // Serialization int Sizeof(); bool Serialize(LFile &f, bool Write); // Tree Item const char *GetText(int i=0) override; int GetImage(int Flags = 0) override; void OnExpand(bool b) override; bool OnKey(LKey &k) override; void Update() override; // Drag'n'drop bool GetFormats(LDragFormats &Formats) override; bool OnBeginDrag(LMouse &m) override; void OnEndData() override; bool GetData(LArray &Data) override; void OnReceiveFiles(LArray &Files); // Import/Export bool GetFormats(bool Export, LString::Array &MimeTypes); IoProgress Import(IoProgressFnArgs); IoProgress Export(IoProgressFnArgs); void ExportAsync(LAutoPtr f, const char *MimeType, std::function Callback = NULL); const char *GetStorageMimeType(); // Dom bool GetVariant(const char *Name, LVariant &Value, const char *Array = NULL) override; bool SetVariant(const char *Name, LVariant &Value, const char *Array = NULL) override; bool CallMethod(const char *MethodName, LVariant *ReturnValue, LArray &Args) override; }; ////////////////////////////////////////////////////////////// class Filter; class FilterCondition { protected: bool TestData(Filter *F, LVariant &v, LStream *Log); public: // Data LAutoString Source; // Data Source (used to be "int Field") LAutoString Value; // Constant char Op; uint8_t Not; // Methods FilterCondition(); bool Set(class LXmlTag *t); // Test condition against email bool Test(Filter *F, Mail *m, LStream *Log); FilterCondition &operator =(FilterCondition &c); // Object ThingUi *DoUI(MailContainer *c = 0); }; class FilterAction : public LListItem, public LDataPropI { LCombo *TypeCbo; LEdit *ArgEdit; LButton *Btn; public: // Data FilterActionTypes Type; LAutoString Arg1; // Methods FilterAction(LDataStoreI *Store); ~FilterAction(); bool Set(LXmlTag *t); bool Get(LXmlTag *t); bool Do(Filter *F, ScribeWnd *App, Mail *&m, LStream *log); void Browse(ScribeWnd *App, LView *Parent); void DescribeHtml(Filter *Flt, LStream &s); LDataPropI &operator =(LDataPropI &p); // List item const char *GetText(int Col = 0); void OnMeasure(LPoint *Info); bool Select(); void Select(bool b); void OnPaintColumn(LItem::ItemPaintCtx &Ctx, int i, LItemColumn *c); int OnNotify(LViewI *c, LNotification n); // Object ThingUi *DoUI(MailContainer *c = 0); // bool Serialize(ObjProperties &f, bool Write); }; class ScribeClass Filter : public Thing { friend class FilterUi; protected: class FilterUi *Ui; class FilterPrivate *d; static int MaxIndex; // Ui LListItemCheckBox *ChkIncoming; LListItemCheckBox *ChkOutgoing; LListItemCheckBox *ChkInternal; bool IgnoreCheckEvents; // Current Mail **Current; // XML I/O LAutoPtr ConditionsCache; LAutoPtr Parse(bool Actions); // Methods bool EvaluateTree(LXmlTag *n, Mail *m, bool &Stop, LStream *Log); bool EvaluateXml(Mail *m, bool &Stop, LStream *Log); public: Filter(ScribeWnd *app, LDataI *object = 0); ~Filter(); LDATA_STR_PROP(Name, FIELD_FILTER_NAME); LDATA_STR_PROP(ConditionsXml, FIELD_FILTER_CONDITIONS_XML); LDATA_STR_PROP(ActionsXml, FIELD_FILTER_ACTIONS_XML); LDATA_STR_PROP(Script, FIELD_FILTER_SCRIPT); LDATA_INT32_PROP(Index, FIELD_FILTER_INDEX); LDATA_INT32_PROP(StopFiltering, FIELD_STOP_FILTERING); LDATA_INT32_PROP(Incoming, FIELD_FILTER_INCOMING); LDATA_INT32_PROP(Outgoing, FIELD_FILTER_OUTGOING); LDATA_INT32_PROP(Internal, FIELD_FILTER_INTERNAL); int Compare(LListItem *Arg, ssize_t Field) override; Thing &operator =(Thing &c) override; Filter *IsFilter() override { return this; } static void Reindex(ScribeFolder *Folder); int *GetDefaultFields() override; const char *GetFieldText(int Field) override; // Methods void Empty(); LAutoString DescribeHtml(); // Import / Export char *GetDropFileName() override; bool GetDropFiles(LString::Array &Files) override; bool GetFormats(bool Export, LString::Array &MimeTypes) override; IoProgress Import(IoProgressFnArgs) override; IoProgress Export(IoProgressFnArgs) override; // Dom bool Evaluate(char *s, LVariant &v); bool SetVariant(const char *Name, LVariant &Value, const char *Array = NULL) override; bool GetVariant(const char *Name, LVariant &Value, const char *Array = NULL) override; bool CallMethod(const char *MethodName, LVariant *ReturnValue, LArray &Args) override; // Filter bool Test(Mail *m, bool &Stop, LStream *Log = 0); bool DoActions(Mail *&m, bool &Stop, LStream *Log = 0); Mail *GetCurrent() { return Current?*Current:0; } /// This filters all the mail in 'Email'. Anything that is handled by a filter /// is removed from the list, leaving just the unfiltered mail. static int ApplyFilters ( /// [In] The window of the filtering caller LView *Parent, /// [In] List of all the filter to test and/or apply List &Filters, /// [In/Out] The email to filter. After the call anything that has been /// acted on by a filter will be removed from the list. List &Email ); // Object Store3ItemTypes Type() override { return MAGIC_FILTER; } ThingUi *DoUI(MailContainer *c = 0) override; bool Serialize(LFile &f, bool Write); bool Save(ScribeFolder *Into = 0) override; void OnMouseClick(LMouse &m) override; void AddAction(FilterAction *a); // List item const char *GetText(int i) override; int GetImage(int Flags) override; void OnPaint(ItemPaintCtx &Ctx) override; void OnColumnNotify(int Col, int64 Data) override; // Index Filter *GetFilterAt(int Index); }; ////////////////////////////////////////////////////////////////////// class Accountlet; enum AccountThreadState { ThreadIdle, ThreadSetup, ThreadConnecting, ThreadTransfer, ThreadWaiting, ThreadDeleting, ThreadDone, ThreadCancel, ThreadError }; enum ReceiveAction { MailNoop, MailDelete, MailDownloadAndDelete, MailDownload, MailUpload, MailHeaders, }; enum ReceiveStatus { MailReceivedNone, MailReceivedWaiting, // This has been given to the main thread MailReceivedOk, // and one of "Ok" or "Error" has to be MailReceivedError, // set to continue. MailReceivedMax, }; ScribeFunc const char *AccountThreadStateName(AccountThreadState i); ScribeFunc const char *ReceiveActionName(ReceiveAction i); ScribeFunc const char *ReceiveStatusName(ReceiveStatus i); class LScribeMime : public LMime { public: LScribeMime() : LMime(ScribeTempPath()) { } }; class LMimeStream : public LTempStream, public LScribeMime { public: LMimeStream(); bool Parse(); }; class MailTransferEvent { public: // Message LAutoPtr Rfc822Msg; ReceiveAction Action = MailNoop; ReceiveStatus Status = MailReceivedNone; int Index = 0; bool Explicit = false; LString Uid; int64 Size = 0; int64 StartWait = 0; // Header Listing class AccountMessage *Msg = NULL; LList *GetList(); // Sending ScribeEnvelope *Send = NULL; LString OutgoingHeaders; // Other class Accountlet *Account = NULL; MailTransferEvent() { } ~MailTransferEvent() { #ifndef LGI_STATIC // LStackTrace("%p::~MailTransferEvent\n", this); #endif } }; #define RoProp(Type, Name) \ protected: \ Type Name; \ public: \ Type Get##Name() { return Name; } class AccountThread : public LThread, public LCancel { protected: Accountlet *Acc; void OnAfterMain(); public: // Object AccountThread(Accountlet *acc); ~AccountThread(); // Api Accountlet *GetAccountlet() { return Acc; } // 1st phase virtual void Disconnect(); // 2nd phase virtual bool Kill(); }; #define AccStrOption(func, opt) \ LVariant func(const char *Set = 0) { LVariant v; StrOption(opt, v, Set); return v; } #define AccIntOption(name, opt) \ int name(int Set = -1) { LVariant v; IntOption(opt, v, Set); return v.CastInt32(); } class ScribeClass Accountlet : public LStream { friend class ScribeAccount; friend class AccountletThread; friend class AccountThread; public: struct AccountletPriv { LArray Log; }; class AccountletLock { LMutex *l; public: bool Locked; AccountletPriv *d; AccountletLock(AccountletPriv *data, LMutex *lck, const char *file, int line) { d = data; l = lck; Locked = lck->Lock(file, line); } ~AccountletLock() { if (Locked) l->Unlock(); } }; typedef LAutoPtr I; private: AccountletPriv d; LMutex PrivLock; protected: // Data ScribeAccount *Account; LAutoPtr Thread; bool ConnectionStatus; MailProtocol *Client; uint64 LastOnline; LString TempPsw; bool Quiet; LView *Parent; // Pointers ScribeFolder *Root; LDataStoreI *DataStore; LMailStore *MailStore; // this memory is owned by ScribeWnd // Options const char *OptPassword; // Members LSocketI *CreateSocket(bool Sending, LCapabilityClient *Caps, bool RawLFCheck); bool WaitForTransfers(List &Files); void StrOption(const char *Opt, LVariant &v, const char *Set); void IntOption(const char *Opt, LVariant &v, int Set); // LStringPipe Line; ssize_t Write(const void *buf, ssize_t size, int flags); public: MailProtocolProgress Group; MailProtocolProgress Item; Accountlet(ScribeAccount *a); ~Accountlet(); Accountlet &operator =(const Accountlet &a) { LAssert(0); return *this; } I Lock(const char *File, int Line) { I a(new AccountletLock(&d, &PrivLock, File, Line)); if (!a->Locked) a.Reset(); return a; } // Methods bool Connect(LView *Parent, bool Quiet); bool Lock(); void Unlock(); virtual bool IsConfigured() { return false; } bool GetStatus() { return ConnectionStatus; } uint64 GetLastOnline() { return LastOnline; } ScribeAccount* GetAccount() { return Account; } bool IsCancelled(); void IsCancelled(bool b); ScribeWnd* GetApp(); const char* GetStateName(); ScribeFolder* GetRootFolder() { return Root; } RoProp(AccountThreadState, State); bool IsOnline() { if (DataStore) return DataStore->GetInt(FIELD_IS_ONLINE) != 0; return Thread != 0; } void OnEndSession() { if (DataStore) DataStore->SetInt(FIELD_IS_ONLINE, false); } // Commands void Disconnect(); void Kill(); // Data char *OptionName(const char *Opt, char *Dest, int DestLen); void Delete(); LThread *GetThread() { return Thread; } LMailStore *GetMailStore() { return MailStore; } LDataStoreI *GetDataStore() { return DataStore; } // General options virtual int UseSSL(int Set = -1) = 0; AccStrOption(Name, OPT_AccountName); AccIntOption(Disabled, OPT_AccountDisabled); AccIntOption(Id, OPT_AccountUID); AccIntOption(Expanded, OPT_AccountExpanded); bool GetPassword(GPassword *p); void SetPassword(GPassword *p); bool IsCheckDialup(); // Events void OnThreadDone(); void OnOnlineChange(bool Online); virtual void OnBeforeDelete(); // LDom impl bool GetVariant(const char *Name, LVariant &Value, const char *Array = NULL); bool SetVariant(const char *Name, LVariant &Value, const char *Array = NULL); // Virtuals virtual void Main(AccountletThread *Thread) = 0; virtual LVariant Server(const char *Set = 0) = 0; virtual LVariant UserName(const char *Set = 0) = 0; virtual void Enabled(bool b) = 0; virtual void OnPulse(char *s, int s_len) {} virtual bool IsReceive() { return false; } virtual bool InitMenus() { return false; } virtual void CreateMaps() = 0; virtual ScribeAccountletStatusIcon GetStatusIcon() { return STATUS_ERROR; } }; #undef RoProp class AccountletThread; class AccountIdentity : public Accountlet { public: AccountIdentity(ScribeAccount *a); AccStrOption(Name, OPT_AccIdentName); AccStrOption(Email, OPT_AccIdentEmail); AccStrOption(ReplyTo, OPT_AccIdentReply); AccStrOption(TextSig, OPT_AccIdentTextSig); AccStrOption(HtmlSig, OPT_AccIdentHtmlSig); AccIntOption(Sort, OPT_AccountSort); int UseSSL(int Set = -1) { return 0; } void Main(AccountletThread *Thread) {} LVariant Server(const char *Set = 0) { return LVariant(); } LVariant UserName(const char *Set = 0) { return LVariant(); } void Enabled(bool b) {} void CreateMaps(); bool IsValid(); bool GetVariant(const char *Name, LVariant &Value, const char *Array = NULL); bool SetVariant(const char *Name, LVariant &Value, const char *Array = NULL); }; struct ScribeEnvelope { LString MsgId; LString SourceFolder; LString From; LArray To; LString References; LString FwdMsgId; LString BounceMsgId; LString Rfc822; }; class SendAccountlet : public Accountlet { friend class ScribeAccount; friend class ScribeWnd; LMenuItem *SendItem; public: LArray Outbox; SendAccountlet(ScribeAccount *a); ~SendAccountlet(); // Methods void Main(AccountletThread *Thread); void Enabled(bool b); bool InitMenus(); void CreateMaps(); ScribeAccountletStatusIcon GetStatusIcon(); // Sending options AccStrOption(Server, OPT_SmtpServer); AccIntOption(Port, OPT_SmtpPort); AccStrOption(Domain, OPT_SmtpDomain); AccStrOption(UserName, OPT_SmtpName); AccIntOption(RequireAuthentication, OPT_SmtpAuth); AccIntOption(AuthType, OPT_SmtpAuthType); AccStrOption(PrefCharset1, OPT_SendCharset1); AccStrOption(PrefCharset2, OPT_SendCharset2); AccStrOption(HotFolder, OPT_SendHotFolder); AccIntOption(OnlySendThroughThisAccount, OPT_OnlySendThroughThis); /// Get/Set the SSL mode /// \sa #SSL_NONE, #SSL_STARTTLS or #SSL_DIRECT AccIntOption(UseSSL, OPT_SmtpSSL); bool IsConfigured() { LVariant hot = HotFolder(); if (hot.Str()) { bool Exists = LDirExists(hot.Str()); printf("%s:%i - '%s' exists = %i\n", _FL, hot.Str(), Exists); return Exists; } return ValidStr(Server().Str()); } bool GetVariant(const char *Name, LVariant &Value, const char *Array = NULL); bool SetVariant(const char *Name, LVariant &Value, const char *Array = NULL); }; typedef LHashTbl, LXmlTag*> MsgListHash; class MsgList : protected MsgListHash { LOptionsFile *Opts; LString Tag; bool Loaded; bool Load(); LXmlTag *LockId(const char *id, const char *file, int line); void Unlock(); public: typedef MsgListHash Parent; bool Dirty; MsgList(LOptionsFile *Opts, char *Tag); ~MsgList(); // Access methods bool Add(const char *id); bool Delete(const char *id); int Length(); bool Find(const char *id); void Empty(); LString::Array CopyKeys(); // Dates bool SetDate(char *id, LDateTime *dt); bool GetDate(char *id, LDateTime *dt); }; class ReceiveAccountlet : public Accountlet { friend class ScribeAccount; friend class ScribeWnd; friend class ImapThread; friend class ScpThread; LArray Actions; LList *Items; int SecondsTillOnline; LMenuItem *ReceiveItem; LMenuItem *PreviewItem; List *IdTemp; LAutoPtr SettingStore; LAutoPtr Msgs; LAutoPtr Spam; public: ReceiveAccountlet(ScribeAccount *a); ~ReceiveAccountlet(); // Props LList *GetItems() { return Items; } bool SetItems(LList *l); bool SetActions(LArray *a = NULL); bool IsReceive() { return true; } bool IsPersistant(); // Methods void Main(AccountletThread *Thread); void OnPulse(char *s, int s_len); bool OnIdle(); void Enabled(bool b); bool InitMenus(); int GetCheckTimeout(); void CreateMaps(); ScribeAccountletStatusIcon GetStatusIcon(); // Message list bool HasMsg(const char *Id); void AddMsg(const char *Id); void RemoveMsg(const char *Id); void RemoveAllMsgs(); int GetMsgs(); // Spam list void DeleteAsSpam(const char *Id); bool RemoveFromSpamIds(const char *Id); bool IsSpamId(const char *Id, bool Delete = false); // Receive options AccStrOption(Protocol, OPT_Pop3Protocol); ScribeProtocol ProtocolType() { return ProtocolStrToEnum(Protocol().Str()); } AccStrOption(Server, OPT_Pop3Server); AccIntOption(Port, OPT_Pop3Port); AccStrOption(UserName, OPT_Pop3Name); AccStrOption(DestinationFolder, OPT_Pop3Folder); AccIntOption(AutoReceive, OPT_Pop3AutoReceive); AccStrOption(CheckTimeout, OPT_Pop3CheckEvery); AccIntOption(LeaveOnServer, OPT_Pop3LeaveOnServer); AccIntOption(DeleteAfter, OPT_DeleteAfter); AccIntOption(DeleteDays, OPT_DeleteDays); AccIntOption(DeleteLarger, OPT_DeleteIfLarger); AccIntOption(DeleteSize, OPT_DeleteIfLargerSize); AccIntOption(DownloadLimit, OPT_MaxEmailSize); AccStrOption(Assume8BitCharset, OPT_Receive8BitCs); AccStrOption(AssumeAsciiCharset, OPT_ReceiveAsciiCs); AccIntOption(AuthType, OPT_ReceiveAuthType); AccStrOption(HotFolder, OPT_ReceiveHotFolder); AccIntOption(SecureAuth, OPT_ReceiveSecAuth); /// Get/Set the SSL mode /// \sa #SSL_NONE, #SSL_STARTTLS or #SSL_DIRECT AccIntOption(UseSSL, OPT_Pop3SSL); bool IsConfigured() { LVariant hot = HotFolder(); if (hot.Str() && LDirExists(hot.Str())) { return true; } LVariant v = Server(); bool s = ValidStr(v.Str()); v = Protocol(); if (!v.Str() || _stricmp(v.Str(), PROTOCOL_POP_OVER_HTTP) != 0) { v = UserName(); s &= ValidStr(v.Str()); } return s; } bool GetVariant(const char *Name, LVariant &Value, const char *Array = NULL); bool SetVariant(const char *Name, LVariant &Value, const char *Array = NULL); }; class ScribeAccount : public LDom, public LXmlTreeUi, public LCapabilityClient { friend class ScribeWnd; friend class ScribePopViewer; friend class AccountStatusPanel; friend class Accountlet; friend class SendAccountlet; friend class ReceiveAccountlet; protected: class ScribeAccountPrivate *d; ScribeWnd *Parent; ScribeFolder *&GetRoot(); void SetIndex(int i); public: // Data AccountIdentity Identity; SendAccountlet Send; ReceiveAccountlet Receive; LArray Views; // Object ScribeAccount(ScribeWnd *parent, int index); ~ScribeAccount(); // Lifespan bool IsValid(); bool Create(); bool Delete(); // Properties ScribeWnd *GetApp() { return Parent; } int GetIndex(); bool IsOnline(); void SetCheck(bool c); LMenuItem *GetMenuItem(); void SetMenuItem(LMenuItem *i); void OnEndSession() { Send.OnEndSession(); Receive.OnEndSession(); } // Commands void Stop(); bool Disconnect(); void Kill(); void SetDefaults(); // User interface void InitUI(LView *Parent, int Tab, std::function callback); bool InitMenus(); void SerializeUi(LView *Wnd, bool Load); int OnNotify(LViewI *Ctrl, LNotification &n); // Worker void OnPulse(char *s = NULL, int s_len = 0); void ReIndex(int i); void CreateMaps(); // LDom interface bool GetVariant(const char *Name, LVariant &Value, const char *Array = NULL) override; bool SetVariant(const char *Name, LVariant &Value, const char *Array = NULL) override; bool CallMethod(const char *MethodName, LVariant *ReturnValue, LArray &Args) override; }; ////////////////////////////////////////////////////////////////////// class ScribeClass ScribeDom : public LDom { ScribeWnd *App; public: Mail *Email; Contact *Con; ContactGroup *Grp; Calendar *Cal; Filter *Fil; ScribeDom(ScribeWnd *a); bool GetVariant(const char *Name, LVariant &Value, const char *Array = 0); }; ////////////////////////////////////////////////////////////////////// #include "BayesianFilter.h" #include "Components.h" class LMailStore { public: bool Default, Expanded; LString Name; LString Path; LDataStoreI *Store; ScribeFolder *Root; LMailStore() { Expanded = true; Default = false; Store = NULL; Root = NULL; } bool IsOk() { return Store != NULL && Root != NULL; } int Priority() { int Ver = Store ? (int)Store->GetInt(FIELD_VERSION) : 0; return Ver; } LMailStore &operator =(LMailStore &a) { LAssert(0); return *this; } }; struct OptionsInfo { LString File; char *Leaf; int Score; uint64 Mod; bool Usual; OptionsInfo(); OptionsInfo &operator =(char *p); LOptionsFile *Load(); }; class ScribeClass ScribeWnd : public LWindow, public LDom, public LDataEventsI, public BayesianFilter, public CapabilityInstaller, public LCapabilityTarget { friend class ScribeAccount; friend class Accountlet; friend class SendAccountlet; friend class ReceiveAccountlet; friend class AccountStatusPanel; friend class ScribeFolder; friend class OptionsDlg; friend class LoadWordStoreThread; friend struct ScribeReplicator; public: enum LayoutMode { OptionsLayout = 0, ///-------------------- /// | | /// Folders | List | /// | | /// |---------| /// | Preview | ///-------------------- FoldersListAndPreview = 1, ///----------------- /// | | /// Folders | List | /// | | ///----------------- /// Preview | ///----------------- PreviewOnBottom, ///----------------- /// | | /// Folders | List | /// | | ///----------------- FoldersAndList, ///--------------------------- /// | | | /// Folders | List | Preview | /// | | | ///--------------------------- ThreeColumn, }; enum AppState { ScribeConstructing, // In Construct1 + Construct2 ScribeConstructed, // Finished Construct2 and ready for Construct3 ScribeInitializing, // In Construct3 ScribeRunning, ScribeExiting, ScribeLoadingFolders, ScribeUnloadingFolders, }; AppState GetScribeState() { return ScribeState; } protected: class ScribeWndPrivate *d = NULL; LTrayIcon TrayIcon; // Ipc LSharedMemory *ScribeIpc = NULL; class ScribeIpcInstance *ThisInst = NULL; bool ShutdownIpc(); // Accounts List Accounts; // New Mail stuff class LNewMailDlg *NewMailDlg = NULL; static AppState ScribeState; DoEvery Ticker; int64 LastDrop = 0; // Static LSubMenu *File = NULL; LSubMenu *ContactsMenu = NULL; LSubMenu *Edit = NULL; LSubMenu *Help = NULL; LToolBar *Commands = NULL; // Dynamic LSubMenu *IdentityMenu = NULL; LMenuItem *DefaultIdentityItem = NULL; LSubMenu *MailMenu = NULL; LSubMenu *SendMenu = NULL, *ReceiveMenu = NULL, *PreviewMenu = NULL; LMenuItem *SendItem = NULL, *ReceiveItem = NULL, *PreviewItem = NULL; LSubMenu *NewTemplateMenu = NULL; LMenuItem *WorkOffline = NULL; // Commands LCommand CmdSend; LCommand CmdReceive; LCommand CmdPreview; // Storage LArray Folders; LArray FolderTasks; List PostValidateFree; // Main view LAutoPtr ImageList; LAutoPtr ToolbarImgs; class LBox *Splitter = NULL; ThingList *MailList = NULL; class DynamicHtml *TitlePage = NULL; class LSearchView *SearchView = NULL; MailTree *Tree = NULL; class LPreviewPanel *PreviewPanel = NULL; class AccountStatusPanel *StatusPanel = NULL; // Security ScribePerm CurrentAuthLevel = PermRequireNone; // Methods void SetupUi(); void SetupAccounts(); int AdjustAllObjectSizes(LDataI *Item); bool CleanFolders(ScribeFolder *f); void LoadFolders(std::function Callback); bool LoadMailStores(); bool ProcessFolder(LDataStoreI *&Store, int StoreIdx, char *StoreName); bool UnLoadFolders(); void AddFolderToMru(char *FileName); void AddContactsToMenu(LSubMenu *Menu); bool FindWordDb(char *Out, int OutSize, char *Name); void OnFolderChanged(LDataFolderI *folder); bool ValidateFolder(LMailStore *s, int Id); void GrowlOnMail(Mail *m); void GrowlInfo(LString title, LString text); bool OnTransfer(); ScribeDomType StrToDom(const char *Var) { return ::StrToDom(Var); } const char* DomToStr(ScribeDomType p) { return ::DomToStr(p); } void LoadImageResources(); void DoOnTimer(LScriptCallback *c); public: ScribeWnd(); void Construct1(); void Construct2(); void Construct3(); void SetLanguage(); ~ScribeWnd(); static bool IsUnitTest; const char *GetClass() override { return "ScribeWnd"; } void DoDebug(char *s); void Validate(LMailStore *s); // Dom bool GetVariant(const char *Name, LVariant &Value, const char *Array = NULL) override; bool CallMethod(const char *MethodName, LVariant *ReturnValue, LArray &Args) override; // --------------------------------------------------------------------- // Methods LAutoString GetDataFolder(); LDataStoreI *CreateDataStore(const char *Full, bool CreateIfMissing); Thing *CreateThingOfType(Store3ItemTypes Type, LDataI *obj = 0); Thing *CreateItem(int Type, ScribeFolder *Folder = 0, bool Ui = true); Mail *CreateMail(Contact *c = 0, const char *Email = 0, const char *Name = 0); Mail *LookupMailRef(const char *MsgRef, bool TraceAllUids = false); bool CreateFolders(LAutoString &FileName); bool CompactFolders(LMailStore &Store, bool Interactive = true); void Send(int Which = -1, bool Quiet = false); void Receive(int Which); void Preview(int Which); void OnBeforeConnect(ScribeAccount *Account, bool Receive); void OnAfterConnect(ScribeAccount *Account, bool Receive); bool NeedsCapability(const char *Name, const char *Param = NULL) override; void OnInstall(CapsHash *Caps, bool Status) override; void OnCloseInstaller() override; bool HasFolderTasks() { return FolderTasks.Length() > 0; } int GetEventHandle(); void Update(int What = 0); void UpdateUnRead(ScribeFolder *Folder, int Delta); void ThingPrint(std::function Callback, ThingType *m, LPrinter *Info = NULL, LView *Parent = NULL, int MaxPage = -1); bool OpenAMail(ScribeFolder *Folder); void BuildDynMenus(); LDocView *CreateTextControl(int Id, const char *MimeType, bool Editor, Mail *m = 0); void SetLastDrop() { LastDrop = LCurrentTime(); } void SetListPane(LView *v); void SetLayout(LayoutMode Mode = OptionsLayout); bool IsMyEmail(const char *Email); bool SetItemPreview(LView *v); LOptionsFile::PortableType GetPortableType(); ScribeRemoteContent RemoteContent_GetSenderStatus(const char *Addr); void RemoteContent_ClearCache(); void RemoteContent_AddSender(const char *Addr, bool WhiteList); void SetDefaultHandler(); void OnSetDefaultHandler(bool Error, bool OldAssert); void SetCurrentIdentity(int i=-1); int GetCurrentIdentity(); bool MailReplyTo(Mail *m, bool All = false); bool MailForward(Mail *m); bool MailBounce(Mail *m); void MailMerge(LArray &Recip, const char *FileName, Mail *Source); void OnNewMail(List *NewMailObjs, bool Add = true); void OnNewMailSound(); void OnCommandLine(); void OnTrayClick(LMouse &m) override; void OnTrayMenu(LSubMenu &m) override; void OnTrayMenuResult(int MenuId) override; void OnFolderSelect(ScribeFolder *f); void AddThingSrc(ScribeFolder *src); void RemoveThingSrc(ScribeFolder *src); LArray GetThingSources(Store3ItemTypes Type); bool GetContacts(List &Contacts, ScribeFolder *f = 0, bool Deep = true); List *GetEveryone(); void HashContacts(LHashTbl,Contact*> &Contacts, ScribeFolder *Folder = 0, bool Deep = true); // CapabilityInstaller impl LAutoString GetHttpProxy() override; InstallProgress *StartAction(MissingCapsBar *Bar, LCapabilityTarget::CapsHash *Components, const char *Action) override; class HttpImageThread *GetImageLoader(); int GetMaxPages(); ScribeWnd::LayoutMode GetEffectiveLayoutMode(); ThingList *GetMailList() { return MailList; } // Gets the matching mail store for a given identity LMailStore *GetMailStoreForIdentity ( /// If this is NULL, assume the current identity const char *IdEmail = NULL ); ScribeFolder *GetFolder(int Id, LMailStore *s = NULL, bool Quiet = false); ScribeFolder *GetFolder(int Id, LDataI *s); ScribeFolder *GetFolder(const char *Name, LMailStore *s = NULL); ScribeFolder *GetCurrentFolder(); int GetFolderType(ScribeFolder *f); LImageList *GetIconImgList() { return ImageList; } LAutoPtr &GetToolbarImgList() { return ToolbarImgs; } LString GetResourceFile(SribeResourceType Type); DoEvery *GetTicker() { return &Ticker; } ScribeAccount *GetSendAccount(); ScribeAccount *GetCurrentAccount(); ThingFilter *GetThingFilter(); List *GetAccounts() { return &Accounts; } ScribeAccount *GetAccountById(int Id); ScribeAccount *GetAccountByEmail(const char *Email); LPrinter *GetPrinter(); int GetActiveThreads(); int GetToolbarHeight(); void GetFilters(List &Filters, bool JustIn, bool JustOut, bool JustInternal); bool OnFolderTask(LEventTargetI *Ptr, bool Add); LArray &GetStorageFolders() { return Folders; } LMailStore *GetDefaultMailStore(); LMailStore *GetMailStoreForPath(const char *Path); bool OnMailStore(LMailStore **MailStore, bool Add); ThingList *GetItemList() { return MailList; } LColour GetColour(int i); LMutex *GetLock(); LFont *GetPreviewFont(); LFont *GetBoldFont() { return LSysBold; } LToolBar *LoadToolbar(LViewI *Parent, const char *File, LAutoPtr &Img); class LVmDebuggerCallback *GetDebuggerCallback(); class GpgConnector *GetGpgConnector(); void GetUserInput(LView *Parent, LString Msg, bool Password, std::function Callback); int GetCalendarSources(LArray &Sources); Store3Status GetAccessLevel(LViewI *Parent, ScribePerm Required, const char *ResourceName, std::function Callback); void GetAccountSettingsAccess(LViewI *Parent, ScribeAccessType AccessType, std::function Callback); const char* EditCtrlMimeType(); LAutoString GetReplyXml(const char *MimeType); LAutoString GetForwardXml(const char *MimeType); bool GetHelpFilesPath(char *Path, int PathSize); bool LaunchHelp(const char *File); LAutoString ProcessSig(Mail *m, char *Xml, const char *MimeType); LString ProcessReplyForwardTemplate(Mail *m, Mail *r, char *Xml, int &Cursor, const char *MimeType); bool LogFilterActivity(); ScribeFolder *FindContainer(LDataFolderI *f); bool SaveDirtyObjects(int TimeLimitMs = 100); class LSpellCheck *CreateSpellObject(); class LSpellCheck *GetSpellThread(bool OverrideOpt = false); bool SetSpellThreadParams(LSpellCheck *Thread); void OnSpellerSettingChange(); bool OnMailTransferEvent(MailTransferEvent *e); LViewI *GetView() { return this; } char *GetUiTags(); struct MailStoreUpgradeParams { LAutoString OldFolders; LAutoString NewFolders; bool Quiet; LStream *Log; MailStoreUpgradeParams() { Quiet = false; Log = 0; } }; // Scripting support bool GetScriptCallbacks(LScriptCallbackType Type, LArray &Callbacks); LScriptCallback GetCallback(const char *CallbackMethodName); bool RegisterCallback(LScriptCallbackType Type, LScriptArguments &Args); LStream *ShowScriptingConsole(); bool ExecuteScriptCallback(LScriptCallback &c, LScriptArguments &Args, bool ReturnArgs = false); LScriptEngine *GetScriptEngine(); // Options LOptionsFile *GetOptions(bool Create = false) override; bool ScanForOptionsFiles(LArray &Inf, LSystemPath PathType); bool LoadOptions(); bool SaveOptions(); bool IsSending() { return false; } bool ShowToolbarText(); bool IsValid(); // Data events from storage back ends bool GetSystemPath(int Folder, LVariant &Path) override; void OnNew(LDataFolderI *parent, LArray &new_items, int pos, bool is_new) override; bool OnDelete(LDataFolderI *parent, LArray &items) override; bool OnMove(LDataFolderI *new_parent, LDataFolderI *old_parent, LArray &Items) override; void SetContext(const char *file, int line) override; bool OnChange(LArray &items, int FieldHint) override; void Post(LDataStoreI *store, void *Param) override { PostEvent(M_STORAGE_EVENT, store->Id, (LMessage::Param)Param); } void OnPropChange(LDataStoreI *store, int Prop, LVariantType Type) override; bool Match(LDataStoreI *store, LDataPropI *Addr, int Type, LArray &Matches) override; ContactGroup *FindGroup(char *Name); bool AddStore3EventHandler(LDataEventsI *callback); bool RemoveStore3EventHandler(LDataEventsI *callback); // --------------------------------------------------------------------- // Events void OnDelete(); int OnNotify(LViewI *Ctrl, LNotification n) override; void OnPaint(LSurface *pDC) override; LMessage::Result OnEvent(LMessage *Msg) override; int OnCommand(int Cmd, int Event, OsView Handle) override; void OnPulse() override; void OnPulseSecond(); bool OnRequestClose(bool OsShuttingDown) override; void OnSelect(List *l = 0, bool ChangeEvent = false); void OnReceiveFiles(LArray &Files) override; void OnUrl(const char *Url) override; void OnZoom(LWindowZoom Action) override; void OnCreate() override; void OnMinute(); void OnHour(); bool OnIdle(); void OnBayesAnalyse(const char *Msg, const char *WhiteListEmail) override; /// \returns true if spam bool OnBayesResult(const char *MailRef, double Rating) override; /// \returns true if spam bool OnBayesResult(Mail *m, double Rating); void OnScriptCompileError(const char *Source, Filter *f); }; //////////////////////////////////////////////////////////////////////////////////// #ifdef SCRIBE_APP #include "ScribePrivate.h" #endif diff --git a/Code/ScribeApp.cpp b/Code/ScribeApp.cpp --- a/Code/ScribeApp.cpp +++ b/Code/ScribeApp.cpp @@ -1,12729 +1,12728 @@ /* ** FILE: ScribeApp.cpp ** AUTHOR: Matthew Allen ** DATE: 22/10/1998 ** DESCRIPTION: Scribe email application ** ** Copyright (C) 1998, Matthew Allen ** fret@memecode.com */ // Debug defines // #define PRINT_OUT_STORAGE_TREE // #define TEST_OBJECT_SIZE #define USE_SPELLCHECKER 1 #define USE_INTERNAL_BROWSER 1 // for help #define RUN_STARTUP_SCRIPTS 1 #define PROFILE_ON_PULSE 0 #define TRAY_CONTACT_BASE 1000 #define TRAY_MAIL_BASE 10000 // Includes #include #include #include #include #include #include "Scribe.h" #include "lgi/common/StoreConvert1To2.h" #include "lgi/common/NetTools.h" #include "lgi/common/ProgressDlg.h" #include "lgi/common/TextLabel.h" #include "lgi/common/Button.h" #include "lgi/common/CheckBox.h" #include "lgi/common/OpenSSLSocket.h" #include "lgi/common/SoftwareUpdate.h" #include "lgi/common/Html.h" #include "lgi/common/TextView3.h" #include "lgi/common/RichTextEdit.h" #include "lgi/common/Browser.h" #include "lgi/common/ClipBoard.h" #include "lgi/common/Store3.h" #include "lgi/common/Growl.h" #include "lgi/common/Edit.h" #include "lgi/common/Box.h" #include "lgi/common/LgiRes.h" #include "lgi/common/SpellCheck.h" #include "lgi/common/SubProcess.h" #include "lgi/common/CssTools.h" #include "lgi/common/Map.h" #include "lgi/common/Charset.h" #include "lgi/common/RefCount.h" #include "ScribePrivate.h" #include "PreviewPanel.h" #include "ScribeStatusPanel.h" #include "ScribeFolderDlg.h" #include "ScribePageSetup.h" #include "Calendar.h" #include "CalendarView.h" #include "ScribeSpellCheck.h" #include "Store3Common.h" #include "PrintContext.h" #include "resource.h" #include "ManageMailStores.h" #include "ReplicateDlg.h" #include "ScribeAccountPreview.h" #include "Encryption/GnuPG.h" #include "Store3Webdav/WebdavStore.h" #include "../Resources/resdefs.h" #include "../UnitTests/UnitTest.h" #include "../src/common/Coding/ScriptingPriv.h" #define DEBUG_STORE_EVENTS 0 #if DEBUG_STORE_EVENTS #define LOG_STORE(...) LgiTrace(__VA_ARGS__) #else #define LOG_STORE(...) #endif #define IDM_LOAD_MSG 2000 #define RAISED_LOOK 0 #define SUNKEN_LOOK false #ifdef MAC #define SUNKEN_CTRL false #else #define SUNKEN_CTRL true #endif #if LGI_CARBON #define TRAY_ICON_NONE -1 #define TRAY_ICON_NORMAL -1 #define TRAY_ICON_MAIL 0 #define TRAY_ICON_ERROR 1 #elif defined(WIN32) #define TRAY_ICON_NORMAL 0 #define TRAY_ICON_ERROR 1 #define TRAY_ICON_MAIL 2 #define TRAY_ICON_NONE 3 #else #define TRAY_ICON_NONE -1 #define TRAY_ICON_NORMAL 0 #define TRAY_ICON_ERROR 1 #define TRAY_ICON_MAIL 2 #endif char ScribeThingList[] = "com.memecode.ThingList"; ScribeClipboardFmt *ScribeClipboardFmt::Alloc(bool ForFolders, size_t Size) { ScribeClipboardFmt *obj = (ScribeClipboardFmt*) calloc(sizeof(ScribeClipboardFmt)+((Size-1)*sizeof(Thing*)), 1); if (obj) { memcpy(obj->Magic, ForFolders ? ScribeFolderMagic : ScribeThingMagic, sizeof(obj->Magic)); obj->ProcessId = LAppInst->GetProcessId(); obj->Len = (uint32_t)Size; } return obj; } ScribeClipboardFmt *ScribeClipboardFmt::Alloc(List &Lst) { ScribeClipboardFmt *Fmt = Alloc(false, Lst.Length()); for (unsigned i=0; iThingAt(i, Lst[i]); return Fmt; } ScribeClipboardFmt *ScribeClipboardFmt::Alloc(LArray &Arr) { ScribeClipboardFmt *Fmt = Alloc(false, Arr.Length()); for (unsigned i=0; iThingAt(i, Arr[i]); return Fmt; } bool ScribeClipboardFmt::Is(const char *Type, void *Ptr, size_t Bytes) { // Do we have the minimum bytes for the structure? if (Bytes >= sizeof(ScribeClipboardFmt) && Ptr != NULL) { ScribeClipboardFmt *This = (ScribeClipboardFmt*)Ptr; // Check the magic is the right value if (memcmp(This->Magic, Type, 4) != 0) return false; // Check it's from this process if (This->ProcessId != LAppInst->GetProcessId()) return false; return true; } return false; } Thing *ScribeClipboardFmt::ThingAt(size_t Idx, Thing *Set) { if (memcmp(Magic, ScribeThingMagic, 4)) return NULL; if (Idx >= Len) return NULL; if (Set) Things[Idx] = Set; return Things[Idx]; } ScribeFolder *ScribeClipboardFmt::FolderAt(size_t Idx, ScribeFolder *Set) { if (memcmp(Magic, ScribeFolderMagic, 4)) return NULL; if (Idx >= Len) return NULL; if (Set) Folders[Idx] = Set; return Folders[Idx]; } size_t ScribeClipboardFmt::Sizeof() { return sizeof(*this) + ((Len - 1) * sizeof(Thing*)); } bool OptionSizeInKiB = false; bool ShowRelativeDates = false; const char *MailAddressDelimiters = "\t\r\n;,"; char16 SpellDelim[] = { ' ', '\t', '\r', '\n', ',', ',', '.', ':', ';', '{', '}', '[', ']', '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '_', '-', '+', '=', '|', '\\', '/', '?', '\"', 0 }; const char *DefaultRfXml = "---------- %s ----------\n" "%s: ()\n" "%s: ()\n" "%s: \n" "%s: \n" "\n" "\n" "\n" "\n"; uchar DateTimeFormats[] = { GDTF_DEFAULT, GDTF_DAY_MONTH_YEAR | GDTF_12HOUR, GDTF_MONTH_DAY_YEAR | GDTF_12HOUR, GDTF_YEAR_MONTH_DAY | GDTF_12HOUR, GDTF_DAY_MONTH_YEAR | GDTF_24HOUR, GDTF_MONTH_DAY_YEAR | GDTF_24HOUR, GDTF_YEAR_MONTH_DAY | GDTF_24HOUR }; SystemFolderInfo SystemFolders[] = { {FOLDER_INBOX, OPT_Inbox, NULL}, {FOLDER_OUTBOX, OPT_Outbox, NULL}, {FOLDER_SENT, OPT_Sent, NULL}, {FOLDER_CONTACTS, OPT_Contacts, NULL}, {FOLDER_TRASH, OPT_Trash, NULL}, {FOLDER_CALENDAR, OPT_Calendar, OPT_HasCalendar}, {FOLDER_TEMPLATES, OPT_Templates, OPT_HasTemplates}, {FOLDER_FILTERS, OPT_Filters, OPT_HasFilters}, {FOLDER_GROUPS, OPT_Groups, OPT_HasGroups}, {FOLDER_SPAM, OPT_SpamFolder, OPT_HasSpam}, {-1, 0, 0} }; ScribeBehaviour *ScribeBehaviour::New(ScribeWnd *app) { return 0; } void ScribeOptionsDefaults(LOptionsFile *f) { if (!f) return; f->CreateTag("Accounts"); f->CreateTag("CalendarUI"); f->CreateTag("CalendarUI.Sources"); f->CreateTag("MailUI"); f->CreateTag("ScribeUI"); f->CreateTag("Plugins"); f->CreateTag("Print"); #define DefaultIntOption(opt, def) { LVariant v; if (!f->GetValue(opt, v)) \ f->SetValue(opt, v = (int)def); } #define DefaultStrOption(opt, def) { LVariant v; if (!f->GetValue(opt, v)) \ f->SetValue(opt, v = def); } DefaultIntOption(OPT_DefaultAlternative, 1); DefaultIntOption(OPT_BoldUnread, 1); DefaultIntOption(OPT_PreviewLines, 1); DefaultIntOption(OPT_AutoDeleteExe, 1); DefaultIntOption(OPT_DefaultReplyAllSetting, MAIL_ADDR_BCC); DefaultIntOption(OPT_BlinkNewMail, 1); DefaultIntOption(OPT_MarkReadAfterSeconds, 5); DefaultStrOption(OPT_BayesThreshold, "0.9"); DefaultIntOption(OPT_SoftwareUpdate, 1); DefaultIntOption(OPT_ResizeImgAttachments, false); DefaultIntOption(OPT_ResizeJpegQual, 80); DefaultIntOption(OPT_ResizeMaxPx, 1024); DefaultIntOption(OPT_ResizeMaxKb, 200); DefaultIntOption(OPT_RegisterWindowsClient, 1); DefaultIntOption(OPT_HasTemplates, 0); DefaultIntOption(OPT_HasCalendar, 1); DefaultIntOption(OPT_HasGroups, 1); DefaultIntOption(OPT_HasFilters, 1); DefaultIntOption(OPT_HasSpam, 0); } const char *Store3ItemTypeName(Store3ItemTypes t) { switch (t) { case MAGIC_NONE: return "MAGIC_NONE"; case MAGIC_BASE: return "MAGIC_BASE"; case MAGIC_MAIL: return "MAGIC_MAIL"; case MAGIC_CONTACT: return "MAGIC_CONTACT"; // case MAGIC_FOLDER: return "MAGIC_FOLDER"; case MAGIC_MAILBOX: return "MAGIC_MAILBOX"; case MAGIC_ATTACHMENT: return "MAGIC_ATTACHMENT"; case MAGIC_ANY: return "MAGIC_ANY"; case MAGIC_FILTER: return "MAGIC_FILTER"; case MAGIC_FOLDER: return "MAGIC_FOLDER"; case MAGIC_CONDITION: return "MAGIC_CONDITION"; case MAGIC_ACTION: return "MAGIC_ACTION"; case MAGIC_CALENDAR: return "MAGIC_CALENDAR"; case MAGIC_ATTENDEE: return "MAGIC_ATTENDEE"; case MAGIC_GROUP: return "MAGIC_GROUP"; default: LAssert(0); break; } return "(error)"; } -void SetRecipients(ScribeWnd *App, char *Start, GDataIt l, EmailAddressType CC) +void SetRecipients(ScribeWnd *App, char *Start, LDataIt l, EmailAddressType CC) { while (Start && *Start) { LString Str; char *End = strchr(Start, ','); if (End) { Str.Set(Start, End-Start); Start = End + 1; } else { Str = Start; Start = 0; } if (Str) { ListAddr *a = new ListAddr(App); if (a) { a->CC = CC; if (_strnicmp(Str, "mailto:", 7) == 0) a->sAddr = Str(7,-1); else a->sAddr = Str; l->Insert(a); } } } } static char SoftwareUpdateUri[] = "http://www.memecode.com/update.php"; enum SoftwareStatus { SwError, SwCancel, SwOutOfDate, SwUpToDate }; static LString ExtractVer(const char *s) { char Buf[256], *Out = Buf; for (const char *In = s; *In && Out < Buf + sizeof(Buf) - 1; In++) { if (*In == ' ') break; if (IsDigit(*In) || *In == '.') *Out++ = *In; } *Out++ = 0; return LString(Buf); } void IsSoftwareUpToDate(LSoftwareUpdate::UpdateInfo &Info, ScribeWnd *Parent, bool WithUI, bool IncBetas, std::function callback) { // Software update? LAutoString Proxy = Parent->GetHttpProxy(); LSoftwareUpdate Update(AppName, SoftwareUpdateUri, Proxy); Update.CheckForUpdate( Info, WithUI?Parent:0, IncBetas, [Info, WithUI, Parent, callback](auto status, auto errorMsg) { if (status) { auto LocalVer = LString(ScribeVer).SplitDelimit("."); LString BuildVer = ExtractVer(Info.Build); LToken OnlineVer(BuildVer, "."); if (OnlineVer.Length() != LocalVer.Length()) { LgiTrace("%s:%i - Invalid online version number \"%s\"\n", _FL, Info.Version.Get()); if (callback) callback(SwError); return; } unsigned i; for (i=0; i o) { if (callback) callback(SwUpToDate); return; } } LDateTime Compile; LToken Date(__DATE__, " "); Compile.Month(LDateTime::MonthFromName(Date[0])); Compile.Day(atoi(Date[1])); Compile.Year(atoi(Date[2])); Compile.SetTime(__TIME__); bool DateGreaterThenCompile = Info.Date > Compile; if (callback) callback(DateGreaterThenCompile ? SwOutOfDate : SwUpToDate); return; } else if (WithUI) { if (Info.Cancel) { if (callback) callback(SwCancel); return; } LgiMsg(Parent, LLoadString(IDS_ERROR_SOFTWARE_UPDATE), AppName, MB_OK, errorMsg); } if (callback) callback(SwError); }); } bool UpgradeSoftware(const LSoftwareUpdate::UpdateInfo &Info, ScribeWnd *Parent, bool WithUI) { bool DownloadUpdate = true; if (WithUI) { char Ds[64]; Info.Date.Get(Ds, sizeof(Ds)); DownloadUpdate = LgiMsg(Parent, LLoadString(IDS_SOFTWARE_UPDATE_DOWNLOAD), AppName, MB_YESNO, (char*)Info.Build, (char*)Info.Uri, Ds) == IDYES; } if (!DownloadUpdate) return false; LAutoString Proxy = Parent->GetHttpProxy(); LSoftwareUpdate Update(AppName, SoftwareUpdateUri, Proxy, ScribeTempPath()); return Update.ApplyUpdate(Info, false, Parent); } void SoftwareUpdate(ScribeWnd *Parent, bool WithUI, bool IncBetas, std::function callback) { // Software update? LSoftwareUpdate::UpdateInfo Info; IsSoftwareUpToDate(Info, Parent, WithUI, IncBetas, [WithUI, Parent, Info, callback](auto s) { if (s == SwUpToDate) { if (WithUI) LgiMsg(Parent, LLoadString(IDS_SOFTWARE_CURRENT), AppName, MB_OK); if (callback) callback(false); // we're up to date } else if (s == SwOutOfDate) { auto status = UpgradeSoftware(Info, Parent, WithUI); if (callback) callback(status); // update is going to happen } }); } const char *AppName = "Scribe"; char HelpFile[] = "index.html"; const char OptionsFileName[] = "ScribeOptions"; const char AuthorEmailAddr[] = "fret@memecode.com"; const char AuthorHomepage[] = "http://www.memecode.com"; const char ApplicationHomepage[] = "http://www.memecode.com/scribe.php"; const char CommercialHomepage[] = "http://www.memecode.com/inscribe.php"; const char FaqHomepage[] = "http://www.memecode.com/scribe/faq.php"; const char *DefaultFolderNames[16]; Store3ItemTypes DefaultFolderTypes[] = { MAGIC_MAIL, // Inbox MAGIC_MAIL, // Outbox MAGIC_MAIL, // Sent MAGIC_ANY, // Trash MAGIC_CONTACT, // Contacts MAGIC_MAIL, // Templates MAGIC_FILTER, // Filters MAGIC_CALENDAR, // Calendar Events MAGIC_GROUP, // Groups MAGIC_MAIL, // Spam MAGIC_NONE, MAGIC_NONE, MAGIC_NONE, MAGIC_NONE }; extern void Log(char *File, char *Str, ...); ////////////////////////////////////////////////////////////////////////////// void LogMsg(char *str, ...) { #ifdef _DEBUG char f[256]; LMakePath(f, sizeof(f), LGetExePath(), "log.txt"); if (str) { char buffer[256]; va_list arg; va_start(arg ,str); vsprintf_s(buffer, sizeof(buffer), str, arg); va_end(arg); LFile File; while (!File.Open(f, O_WRITE)) { LSleep(5); } File.Seek(File.GetSize(), SEEK_SET); File.Write(buffer, strlen(buffer)); } else { FileDev->Delete(f, false); } #endif } LString GetFullAppName(bool Platform) { LString Ret = AppName; if (Platform) { LString s; const char *Build = #ifndef _DEBUG "Release"; #else "Debug"; #endif LArray Ver; int Os = LGetOs(&Ver); const char *OsName = LGetOsName(); if (Os == LGI_OS_WIN9X) { switch (Ver[1]) { case 0: OsName = "Win95"; break; case 10: OsName = "Win98"; break; case 90: OsName = "WinME"; break; } } else if (Os == LGI_OS_WIN32 || Os == LGI_OS_WIN64) { if (Ver[0] < 5) { OsName = "WinNT"; } else if (Ver[0] == 5) { if (Ver[1] == 0) OsName = "Win2k"; else OsName = "WinXP"; } else if (Ver[0] == 6) { if (Ver[1] == 0) OsName = "Vista"; else if (Ver[1] == 1) OsName = "Win7"; else if (Ver[1] == 2) OsName = "Win8"; else if (Ver[1] == 3) OsName = "Win8.1"; } else if (Ver[0] == 10) { OsName = "Win10"; } else if (Ver[0] == 11) { // What's the chances eh? OsName = "Win11"; } } s.Printf(" v%s (%s v", ScribeVer, OsName); Ret += s; for (unsigned i=0; iId); Ret += s; } s.Printf(")"); Ret += s; } return Ret; } bool MatchWord(char *Str, char *Word) { bool Status = false; if (Str && Word) { #define IsWord(c) ( IsDigit(c) || IsAlpha(c) ) for (char *s=stristr(Str, Word); s; s=stristr(s+1, Word)) { char *e = s + strlen(Word); if ( (s<=Str || !IsWord(s[-1]) ) && (e[0] == 0 || !IsWord(e[0])) ) { return true; } } } return Status; } ////////////////////////////////////////////////////////////////////////////// ScribePanel::ScribePanel(ScribeWnd *app, const char *name, int size, bool open) : LPanel(name, size, open) { App = app; } bool ScribePanel::Pour(LRegion &r) { if (App) { SetClosedSize(App->GetToolbarHeight()); } return LPanel::Pour(r); } ////////////////////////////////////////////////////////////////////////////// class NoContactType : public Contact { LString NoFace80Path; LString NoFace160Path; public: NoContactType(ScribeWnd *wnd) : Contact(wnd) { } Thing &operator =(Thing &c) override { return *this; } bool GetVariant(const char *Name, LVariant &Value, const char *Array) override { ScribeDomType Fld = StrToDom(Name); int Px = Array ? atoi(Array) : 80; LString &Str = Px == 160 ? NoFace160Path : NoFace80Path; if (!Str) { LString f; f.Printf("NoFace%i.png", Px); Str = LFindFile(f); LAssert(Str != NULL); // This should always resolve. } if (!Str) return false; if (Fld == SdImageHtml) { LString html; html.Printf("\n", Str.Get()); Value = html; return true; } else if (Fld == SdImage) { Value = Str; return true; } return false; } }; class ScribeWndPrivate : public LBrowser::LBrowserEvents, public LVmDebuggerCallback, public LHtmlStaticInst { LOptionsFile::PortableType InstallMode = LOptionsFile::UnknownMode; public: ScribeWnd *App; uint64 LastTs = 0; int ClipboardFormat = 0; LFont *PreviewFont = NULL; int PrintMaxPages = -1; int NewMailTimeout = -1; bool SendAfterReceive = false; bool IngoreOnClose = false; LAutoString UiTags; LAutoPtr Growl; LArray TrayMenuContacts; bool ExitAfterSend = false; LToolButton *ShowConsoleBtn = NULL; LString MulPassword; LBox *SubSplit = NULL, *SearchSplit = NULL; LArray ThingSources; int LastLayout = 0; LMenuItem *DisableUserFilters = NULL; LOptionsFile *Options = NULL; HttpImageThread *ImageLoader = NULL; int LastMinute = -1, LastHour = -1; LArray Store3EventCallbacks; LAutoPtr PrintOptions; LHashTbl, LString> ResFiles; // These are for the LDataEventsI callbacks to store source context // Mainly for debugging where various events came from. const char *CtxFile = NULL; int CtxLine = 0; // Contact no face images LAutoRefPtr NoContact; // Remote content white/blacklists bool RemoteContent_Init = false; LString::Array RemoteWhiteLst, RemoteBlackLst; // Spell checking int AppWndHnd; LAutoPtr SpellerThread; // Missing caps LCapabilityTarget::CapsHash MissingCaps; MissingCapsBar *Bar = NULL; LString ErrSource; // Script file that has an error. Filter *ErrFilter = NULL; // Filter that has scripting error. // Load state bool FoldersLoaded = false; // Bayesian filter LStringPipe BayesLog; // Thread item processing LArray Transfers; // Scripting... LAutoPtr Engine; LArray Scripts; LArray CurrentScripts; LScript *CurrentScript() { return CurrentScripts.Length() ? CurrentScripts.Last() : NULL; } int NextToolMenuId = IDM_TOOL_SCRIPT_BASE; LAutoPtr ScriptToolbar; LArray OnSecondTimerCallbacks; // Encryption LAutoPtr GpgInst; // Unit tests LAutoPtr UnitTestServer; class ScribeTextControlFactory : public LViewFactory { ScribeWnd *Wnd; LView *NewView(const char *Class, LRect *Pos, const char *Text) { if (!_stricmp(Class, "ScribeTextView")) return Wnd->CreateTextControl(-1, 0, true); return NULL; } public: ScribeTextControlFactory(ScribeWnd *wnd) { Wnd = wnd; } } TextControlFactory; ScribeWndPrivate(ScribeWnd *app) : App(app), TextControlFactory(app) { NoContact = new NoContactType(app); NoContact->DecRef(); // 2->1 AppWndHnd = LEventSinkMap::Dispatch.AddSink(App); #ifdef WIN32 ClipboardFormat = RegisterClipboardFormat( #ifdef UNICODE L"Scribe.Item" #else "Scribe.Item" #endif ); #endif LScribeScript::Inst = new LScribeScript(App); if (Engine.Reset(new LScriptEngine(App, LScribeScript::Inst, this))) Engine->SetConsole(LScribeScript::Inst->GetLog()); } ~ScribeWndPrivate() { // Why do we need this? ~LView will take care of it? // LEventSinkMap::Dispatch.RemoveSink(App); DeleteObj(Options); Scripts.DeleteObjects(); DeleteObj(ImageLoader); Engine.Reset(); DeleteObj(LScribeScript::Inst); } LGrowl *GetGrowl() { if (!Growl && Growl.Reset(new LGrowl)) { LAutoPtr r(new LGrowl::LRegister); r->App = "Scribe"; r->IconUrl = "http://memecode.com/images/scribe/growl-app.png"; LGrowl::LNotifyType &NewMail = r->Types.New(); NewMail.Name = "new-mail"; NewMail.IconUrl = "http://memecode.com/images/scribe/growl-new-mail.png"; NewMail.Enabled = true; LGrowl::LNotifyType &Cal = r->Types.New(); Cal.Name = "calendar"; Cal.IconUrl = "http://memecode.com/images/scribe/growl-calendar.png"; Cal.Enabled = true; LGrowl::LNotifyType &Debug = r->Types.New(); Debug.Name = "debug"; Debug.IconUrl = "http://memecode.com/images/scribe/growl-bug.png"; Debug.Enabled = false; LGrowl::LNotifyType &Info = r->Types.New(); Info.IconUrl = "http://memecode.com/images/scribe/growl-note.png"; Info.Name = "info"; Info.Enabled = true; Growl->Register(r); } return Growl; } LVmDebugger *AttachVm(LVirtualMachine *Vm, LCompiledCode *Code, const char *Assembly) { LVariant v; if (Options) Options->GetValue(OPT_ScriptDebugger, v); if (v.CastInt32()) return new LVmDebuggerWnd(App, this, Vm, Code, NULL); return NULL; } bool CompileScript(LAutoPtr &Output, const char *FileName, const char *Source) { LCompiler c; return c.Compile(Output, Engine->GetSystemContext(), LScribeScript::Inst, FileName, Source, NULL); } bool OnSearch(LBrowser *br, const char *txt) { char Path[256]; if (!App->GetHelpFilesPath(Path, sizeof(Path))) return false; LToken Terms(txt, ", "); LStringPipe p; p.Print("\n

Search Results

\n
    \n"); LDirectory Dir; for (int b = Dir.First(Path, "*.html"); b; b = Dir.Next()) { if (!Dir.IsDir()) { char Path[256]; Dir.Path(Path, sizeof(Path)); LFile f; if (f.Open(Path, O_READ)) { LXmlTree t(GXT_NO_DOM); LXmlTag r; if (t.Read(&r, &f)) { char *PrevName = 0; char PrevUri[256] = ""; for (auto c: r.Children) { if (c->IsTag("a")) { char *Name = c->GetAttr("name"); if (Name) { PrevName = Name; } } else if (c->GetContent()) { bool Hit = false; for (unsigned i=0; !Hit && iGetContent(), Terms[i]) != 0; } if (Hit) { LStringPipe Uri(256); char *Leaf = strrchr(Path, DIR_CHAR); Leaf = Leaf ? Leaf + 1 : Path; Uri.Print("file://%s", Path); if (PrevName) Uri.Print("#%s", PrevName); LAutoString UriStr(Uri.NewStr()); if (_stricmp(UriStr, PrevUri)) { p.Print("
  • %s", UriStr.Get(), Leaf); if (PrevName) p.Print("#%s", PrevName); p.Print("\n"); strcpy_s(PrevUri, sizeof(PrevUri), UriStr); } } } } } } } } p.Print("
\n\n\n"); LAutoString Html(p.NewStr()); br->SetHtml(Html); return true; } void AskUserForInstallMode(std::function callback) { auto Dlg = new LAlert(App, AppName, LLoadString(IDS_PORTABLE_Q), LLoadString(IDS_HELP), LLoadString(IDS_DESKTOP), LLoadString(IDS_PORTABLE)); Dlg->SetButtonCallback(1, [&](auto idx) { App->LaunchHelp("install.html"); }); Dlg->DoModal([callback](auto dlg, auto Btn) { if (Btn == 1) { // Help LAssert(!"Help btn should use callback."); } else if (Btn == 2) { // Desktop if (callback) callback(LOptionsFile::DesktopMode); } else if (Btn == 3) { // Portable if (callback) callback(LOptionsFile::PortableMode); } else { delete dlg; LAppInst->Exit(1); } delete dlg; }); } LOptionsFile::PortableType GetInstallMode() { if (InstallMode == LOptionsFile::UnknownMode) { if (LAppInst->GetOption("portable")) { InstallMode = LOptionsFile::PortableMode; LgiTrace("Selecting portable mode based on -portable switch.\n"); } else if (LAppInst->GetOption("desktop")) { InstallMode = LOptionsFile::DesktopMode; LgiTrace("Selecting portable mode based on -desktop switch.\n"); } } if (InstallMode == LOptionsFile::UnknownMode) { bool PortableIsPossible = true; char Inst[MAX_PATH_LEN] = ""; LGetSystemPath(LSP_APP_INSTALL, Inst, sizeof(Inst)); // Do write check char Wr[MAX_PATH_LEN]; LMakePath(Wr, sizeof(Wr), Inst, "_write_test.txt"); LFile f; if (f.Open(Wr, O_WRITE)) { // Clean up f.Close(); FileDev->Delete(Wr, false); } else { // No write perms PortableIsPossible = false; } if (PortableIsPossible && LAppInst->IsElevated()) { // Check if the install is in some read only location: // e.g. c:\Program Files char Pm[MAX_PATH_LEN]; if (LGetSystemPath(LSP_USER_APPS, Pm, sizeof(Pm))) { size_t n = strlen(Pm); PortableIsPossible = _strnicmp(Pm, Inst, n) != 0; // LgiMsg(App, "%i\n%s\n%s", AppName, MB_OK, PortableIsPossible, Pm, Inst); } else LgiTrace("%s:%i - Failed to get paths.", _FL); } if (PortableIsPossible) { // Basically "ask the user" here... return LOptionsFile::UnknownMode; } else { InstallMode = LOptionsFile::DesktopMode; LgiTrace("Selecting Desktop based on lack of write permissions to install folder.\n"); } } return InstallMode; } void SetInstallMode(LOptionsFile::PortableType t) { InstallMode = t; } void DeleteCallbacks(LArray &Callbacks) { for (unsigned i=0; iGetMenu()->FindItem(Callbacks[i].Param); if (it) { it->Remove(); DeleteObj(it); } } } } }; ////////////////////////////////////////////////////////////////////////////// void UpgradeRfOption(ScribeWnd *App, const char *New, const char *Old, const char *Default) { LVariant v; /* App->GetOptions()->GetValue(New, v); if (v.Str()) { ScribePath *Path = new ScribePath(App, Old); if (Path) { char *Xml = LReadTextFile(*Path); if (Xml) { App->GetOptions()->SetValue(New, v = Xml); DeleteArray(Xml); } App->GetOptions()->DeleteValue(Old); DeleteObj(Path); } } */ if (Default && !App->GetOptions()->GetValue(New, v)) { App->GetOptions()->SetValue(New, v = Default); } } //////////////////////////////////////////////////////////////////////////// ScribeWnd::AppState ScribeWnd::ScribeState = ScribeConstructing; /* * This constructor is a little convoluted, but the basic idea is this: * * - Do some basic init. * - Attempt to load the options (could make portable/desktop mode clear) * - If the portable/desktop mode is unclear ask the user. * - Call AppConstruct1. * - If the UI language is not known, ask the user. * - Call AppConstruct2. * * Each time a dialog is needed the rest of the code needs to be in a callable function. * * It's important to note that the ScribeWnd::OnCreate method needs to be called after * the system Handle() is created, and after any dialogs in the ScribeWnd::ScribeWnd * constructor have finished. */ ScribeWnd::ScribeWnd() : BayesianFilter(this), CapabilityInstaller("Scribe", ScribeVer, "http://memecode.com/components/lookup.php", ScribeTempPath()), TrayIcon(this) { if (_Lock) _Lock->SetName("ScribeWnd"); // init some variables LApp::ObjInstance()->AppWnd = this; LCharsetSystem::Inst()->DetectCharset = ::DetectCharset; d = new ScribeWndPrivate(this); ScribeIpc = new LSharedMemory("Scribe", SCRIBE_INSTANCE_MAX * sizeof(ScribeIpcInstance)); if (ScribeIpc && ScribeIpc->GetPtr()) { ScribeIpcInstance *InstLst = (ScribeIpcInstance*) ScribeIpc->GetPtr(); for (int i=0; iMagic = SCRIBE_INSTANCE_MAGIC; ThisInst->Pid = LProcessId(); // LgiTrace("Install Scribe pid=%i to pos=%i\n", LProcessId(), i); } } } else DeleteObj(ScribeIpc); #ifndef WIN32 printf("%s\n", GetFullAppName(true).Get()); #endif auto Type = d->GetInstallMode(); if (Type == LOptionsFile::UnknownMode) { // This may make the mode more clear... if (LoadOptions()) Type = d->GetInstallMode(); } if (Type == LOptionsFile::UnknownMode) { d->AskUserForInstallMode([this](auto selectedMode) { d->SetInstallMode(selectedMode); if (!d->Options) d->Options = new LOptionsFile(selectedMode, OptionsFileName); Construct1(); }); } else Construct1(); } void ScribeWnd::Construct1() { if (!d->Options && !LoadOptions()) { ScribeState = ScribeExiting; return; } ScribeOptionsDefaults(d->Options); LVariant GlyphSub; if (GetOptions()->GetValue(OPT_GlyphSub, GlyphSub)) { bool UseGlyphSub = GlyphSub.CastInt32() != 0; LSysFont->SubGlyphs(UseGlyphSub); LSysBold->SubGlyphs(UseGlyphSub); LFontSystem::Inst()->SetDefaultGlyphSub(UseGlyphSub); } else { GetOptions()->SetValue(OPT_GlyphSub, GlyphSub = LFontSystem::Inst()->GetDefaultGlyphSub()); } { // Limit the size of the 'Scribe.txt' log file char p[MAX_PATH_LEN]; if (LgiTraceGetFilePath(p, sizeof(p))) { int64 Sz = LFileSize(p); #define MiB * 1024 * 1024 if (Sz > (3 MiB)) FileDev->Delete(p); } } // Process pre-UI options LVariant SizeAdj; int SzAdj = SizeAdj.CastInt32(); if (GetOptions()->GetValue(OPT_UiFontSize, SizeAdj) && (SzAdj = SizeAdj.CastInt32()) >= 0 && SzAdj < 5) { SzAdj -= 2; if (SzAdj) { int Pt = LSysFont->PointSize(); LSysFont->PointSize(Pt + SzAdj); LSysFont->Create(); LSysBold->PointSize(Pt + SzAdj); LSysBold->Create(); LFont *m = LMenu::GetFont(); if (m) { m->PointSize(m->PointSize() + SzAdj); m->Create(); } } } else { GetOptions()->SetValue(OPT_UiFontSize, SizeAdj = 2); } // Resources and languages SetLanguage(); // If no language set... LVariant LangId; if (!GetOptions()->GetValue(OPT_UiLanguage, LangId)) { // Ask the user... auto Dlg = new LanguageDlg(this); if (!Dlg->Ok) { delete Dlg; LgiMsg(this, "Failed to create language selection dialog.", "Scribe Error"); ScribeState = ScribeExiting; LCloseApp(); } else { Dlg->DoModal([this, Dlg](auto dlg, auto id) { if (id) { // Set the language in the options file LVariant v; GetOptions()->SetValue(OPT_UiLanguage, v = Dlg->Lang.Get()); // Reload the resource file... to get the new lang. LResources *Cur = LgiGetResObj(false); DeleteObj(Cur); SetLanguage(); Construct2(); } else // User canceled { ScribeState = ScribeExiting; LCloseApp(); } delete dlg; }); } } else Construct2(); } void ScribeWnd::Construct2() { #if 1 auto CurRes = LgiGetResObj(false); LVariant Theme; if (CurRes && GetOptions()->GetValue(OPT_Theme, Theme)) { auto Paths = ScribeThemePaths(); auto NoTheme = LLoadString(IDS_DEFAULT); if (Theme.Str() && Stricmp(NoTheme, Theme.Str())) { for (auto p: Paths) { LFile::Path Inst(p); Inst += Theme.Str(); if (Inst.Exists()) { CurRes->SetThemeFolder(Inst); d->Static->OnSystemColourChange(); break; } } } } #endif LoadCalendarStringTable(); ZeroObj(DefaultFolderNames); DefaultFolderNames[FOLDER_INBOX] = LLoadString(IDS_FOLDER_INBOX, "Inbox"); DefaultFolderNames[FOLDER_OUTBOX] = LLoadString(IDS_FOLDER_OUTBOX, "Outbox"); DefaultFolderNames[FOLDER_SENT] = LLoadString(IDS_FOLDER_SENT, "Sent"); DefaultFolderNames[FOLDER_TRASH] = LLoadString(IDS_FOLDER_TRASH, "Trash"); DefaultFolderNames[FOLDER_CONTACTS] = LLoadString(IDS_FOLDER_CONTACTS, "Contacts"); DefaultFolderNames[FOLDER_TEMPLATES] = LLoadString(IDS_FOLDER_TEMPLATES, "Templates"); DefaultFolderNames[FOLDER_FILTERS] = LLoadString(IDS_FOLDER_FILTERS, "Filters"); DefaultFolderNames[FOLDER_CALENDAR] = LLoadString(IDS_FOLDER_CALENDAR, "Calendar"); DefaultFolderNames[FOLDER_GROUPS] = LLoadString(IDS_FOLDER_GROUPS, "Groups"); DefaultFolderNames[FOLDER_SPAM] = LLoadString(IDS_SPAM, "Spam"); LStringPipe RfXml; RfXml.Print(DefaultRfXml, LLoadString(IDS_ORIGINAL_MESSAGE), LLoadString(FIELD_TO), LLoadString(FIELD_FROM), LLoadString(FIELD_SUBJECT), LLoadString(IDS_DATE)); { LAutoString Xml(RfXml.NewStr()); UpgradeRfOption(this, OPT_TextReplyFormat, "ReplyXml", Xml); UpgradeRfOption(this, OPT_TextForwardFormat, "ForwardXml", Xml); } LFontType t; if (t.GetSystemFont("small")) { d->PreviewFont = t.Create(); if (d->PreviewFont) { #if defined WIN32 d->PreviewFont->PointSize(8); #endif } } MoveOnScreen(); // Load global graphics LoadImageResources(); // Load time threads // Window name Name(AppName); SetSnapToEdge(true); ClearTempPath(); #if WINNATIVE SetStyle(GetStyle() & ~WS_VISIBLE); SetExStyle(GetExStyle() & ~WS_EX_ACCEPTFILES); CreateClassW32(AppName, LoadIcon(LProcessInst(), MAKEINTRESOURCE(IDI_APP))); #endif #if defined LINUX SetIcon("About64px.png"); LFinishXWindowsStartup(this); #endif ScribeState = ScribeConstructed; OnCreate(); } void ScribeWnd::Construct3() { if (ScribeState == ScribeConstructing) { // Constructor is still running, probably showing some UI. // Don't complete setup at this point. return; } // Load the styles LResources::StyleElement(this); // Main menu Menu = new LMenu(AppName); if (Menu) { Menu->Attach(this); if (Menu->Load(this, "ID_MENU", GetUiTags())) { LAssert(ImageList != NULL); Menu->SetImageList(ImageList, false); auto IdentityItem = Menu->FindItem(IDM_NO_IDENTITIES); if (IdentityItem) { IdentityMenu = IdentityItem->GetParent(); } CmdSend.MenuItem = Menu->FindItem(IDM_SEND_MAIL); auto NewMailMenu = Menu->FindItem(IDM_NEW_EMAIL); if (NewMailMenu) { MailMenu = NewMailMenu->GetParent(); } LVariant v; WorkOffline = Menu->FindItem(IDM_WORK_OFFLINE); if (WorkOffline && GetOptions()->GetValue(OPT_WorkOffline, v)) { WorkOffline->Checked(v.CastInt32() != 0); } if ((d->DisableUserFilters = Menu->FindItem(IDM_FILTERS_DISABLE))) { if (GetOptions()->GetValue(OPT_DisableUserFilters, v)) { d->DisableUserFilters->Checked(v.CastInt32() != 0); } } #if RUN_STARTUP_SCRIPTS // Run scripts in './Scripts' folder char s[MAX_PATH_LEN]; LMakePath(s, sizeof(s), ScribeResourcePath(), "Scripts"); if (!LDirExists(s)) LMakePath(s, sizeof(s), LGetSystemPath(LSP_APP_INSTALL), #if defined(LINUX) || defined(WINDOWS) "..\\" #endif "Scripts"); if (!LDirExists(s)) LgiTrace("%s:%i - Error: the scripts folder '%s' doesn't exist.\n", _FL, s); else { bool ErrorDsp = false; LDirectory Dir; for (int b = Dir.First(s); b; b = Dir.Next()) { if (Dir.IsDir()) continue; char *Ext = LGetExtension(Dir.GetName()); if (!Ext || _stricmp(Ext, "script") != 0) continue; Dir.Path(s, sizeof(s)); LStringPipe Log; char *Source = LReadTextFile(s); if (Source) { LScript *Cur = new LScript; if (Cur) { char Msg[256]; d->CurrentScripts.Add(Cur); LScribeScript::Inst->GetLog()->Write(Msg, sprintf_s(Msg, sizeof(Msg), "Compiling '%s'...\n", Dir.GetName())); LCompiler c; if (c.Compile( Cur->Code, d->Engine->GetSystemContext(), LScribeScript::Inst, s, Source, NULL)) { LFunctionInfo *Main = Cur->Code->GetMethod("Main"); if (Main) { LVirtualMachine Vm(d); LScriptArguments Args(&Vm); Args.New() = new LVariant((LDom*)this); d->Scripts.Add(Cur); if (Vm.ExecuteFunction( Cur->Code, Main, Args, LScribeScript::Inst->GetLog()) && Args.GetReturn()->CastInt32()) { d->CurrentScripts.Delete(Cur, true); Cur = NULL; } else { LgiTrace("Error: Script's main failed (%s)\n", Cur->Code->GetFileName()); if (Cur->Callbacks.Length()) d->DeleteCallbacks(Cur->Callbacks); d->Scripts.Delete(Cur); } Args.DeleteObjects(); } } else if (!ErrorDsp) { ErrorDsp = true; OnScriptCompileError(Source, NULL); } if (Cur) { d->CurrentScripts.Delete(Cur, true); DeleteObj(Cur); } } DeleteArray(Source); } } } #endif #define EnableItem(id, en) { auto i = Menu->FindItem(id); if (i) i->Enabled(en); } #define SetMenuIcon(id, ico) { auto i = Menu->FindItem(id); if (i) i->Icon(ico); } EnableItem(IDM_IMPORT_OUTLOOK_ITEMS, true); // SetMenuIcon(IDM_OPEN_FOLDERS, ICON_OPEN_FOLDER); SetMenuIcon(IDM_OPTIONS, ICON_OPTIONS); SetMenuIcon(IDM_SECURITY, ICON_LOCK); SetMenuIcon(IDM_CUT, ICON_CUT); SetMenuIcon(IDM_COPY, ICON_COPY); SetMenuIcon(IDM_PASTE, ICON_PASTE); SetMenuIcon(IDM_LAYOUT1, ICON_LAYOUT1); SetMenuIcon(IDM_LAYOUT2, ICON_LAYOUT2); SetMenuIcon(IDM_LAYOUT3, ICON_LAYOUT3); SetMenuIcon(IDM_LAYOUT4, ICON_LAYOUT4); SetMenuIcon(IDM_NEW_EMAIL, ICON_UNSENT_MAIL); SetMenuIcon(IDM_SET_READ, ICON_READ_MAIL); SetMenuIcon(IDM_SET_UNREAD, ICON_UNREAD_MAIL); SetMenuIcon(IDM_NEW_CONTACT, ICON_CONTACT); SetMenuIcon(IDM_NEW_GROUP, ICON_CONTACT_GROUP); SetMenuIcon(IDM_REPLY, ICON_FLAGS_REPLY); SetMenuIcon(IDM_REPLY_ALL, ICON_FLAGS_REPLY); SetMenuIcon(IDM_FORWARD, ICON_FLAGS_FORWARD); SetMenuIcon(IDM_BOUNCE, ICON_FLAGS_BOUNCE); SetMenuIcon(IDM_NEW_FILTER, ICON_FILTER); SetMenuIcon(IDM_FILTER_CURRENT_FOLDER, ICON_FOLDER_FILTERS); SetMenuIcon(IDM_MEMECODE, ICON_LINK); SetMenuIcon(IDM_HOMEPAGE, ICON_LINK); SetMenuIcon(IDM_SCRIBE_FAQ, ICON_LINK); SetMenuIcon(IDM_INSCRIBE_LINK, ICON_LINK); SetMenuIcon(IDM_VERSION_HISTORY, ICON_LINK); SetMenuIcon(IDM_DEBUG_INFO, ICON_LINK); SetMenuIcon(IDM_TUTORIALS, ICON_LINK); SetMenuIcon(IDM_FEEDBACK, ICON_UNREAD_MAIL); SetMenuIcon(IDM_HELP, ICON_HELP); LMenuItem *mi; if ( GetOptions()->GetValue(OPT_EditControl, v) && (mi = Menu->FindItem(IDM_HTML_EDITOR)) ) mi->Checked(v.CastInt32() != 0); Menu->SetPrefAndAboutItems(IDM_OPTIONS, IDM_ABOUT); } } // Initialize user interface SetupUi(); // Get some of the base submenu pointers. These are needed // for SetupAccounts to work correctly, e.g. populate the // send/receive/preview submenus. Folders need to be loaded // before this for the templates folder BuildDynMenus(); // Load accounts SetupAccounts(); // Recursively load folder tree LoadFolders([&](auto status) { // Redo it for the templates... now that load folders has completed. BuildDynMenus(); if (ScribeState == ScribeExiting) return; // Process command line OnCommandLine(); // Update the templates sub-menu now that the folders are loaded BuildDynMenus(); // Check registry settings SetDefaultHandler(); // Run on load scripts... LArray OnLoadCallbacks; if (GetScriptCallbacks(LOnLoad, OnLoadCallbacks)) { for (auto r: OnLoadCallbacks) { LVirtualMachine Vm; LScriptArguments Args(&Vm); Args.New() = new LVariant(this); ExecuteScriptCallback(*r, Args); Args.DeleteObjects(); } } ScribeState = ScribeRunning; }); } void ScribeWnd::SetLanguage() { LVariant LangId; if (GetOptions()->GetValue(OPT_UiLanguage, LangId)) { // Set the language to load... LAppInst->SetConfig("Language", LangId.Str()); } LResources::SetLoadStyles(true); // Load the resources (with the current lang) if (!LgiGetResObj(true, "Scribe")) { LgiMsg(NULL, "The resource file 'Scribe.lr8' is missing.", AppName); ScribeState = ScribeExiting; LCloseApp(); } } ScribeWnd::~ScribeWnd() { LAppInst->AppWnd = 0; SearchView = NULL; ScribeState = ScribeExiting; LScribeScript::Inst->ShowScriptingWindow(false); // Other cleanup... ClearTempPath(); ShutdownIpc(); SetPulse(); // Save anything thats still dirty in the folders... // just in case we crash during the shutdown phase. ScribeFolder *Cur = GetCurrentFolder(); if (Cur) Cur->SerializeFieldWidths(); SaveDirtyObjects(5000); // Tell the UI not to reference anything in the folders if (PreviewPanel) { PreviewPanel->OnThing(0, false); } Mail::NewMailLst.Empty(); // ~AccountStatusItem references the account list... must be before we // delete the accounts. DeleteObj(StatusPanel); // ~Accountlet needs to reference the root container... so // it has to go before unloading of folders. Accounts.DeleteObjects(); UnLoadFolders(); DeleteObj(PreviewPanel); SaveOptions(); DeleteObj(Commands); DeleteObj(d->PreviewFont); DeleteObj(d->SubSplit); DeleteObj(Splitter); MailList = NULL; CmdSend.ToolButton = NULL; CmdReceive.ToolButton = NULL; CmdPreview.ToolButton = NULL; CmdSend.MenuItem = NULL; CmdReceive.MenuItem = NULL; CmdPreview.MenuItem = NULL; // This could be using the OpenSSL library for HTTPS connections. So // close it before calling EndSSL. DeleteObj(d->ImageLoader); // This has to be after we close all the accounts... otherwise // they might still be using SSL functions, e.g. an IMAP/SSL connect. EndSSL(); DeleteObj(d); } LString ScribeWnd::GetResourceFile(SribeResourceType Type) { auto File = d->ResFiles.Find(Type); if (!File) LgiTrace("%s:%i - No file for resource type %i\n", _FL, Type); return File; } void ScribeWnd::LoadImageResources() { auto Res = LgiGetResObj(); LString::Array Folders; if (Res) { auto p = Res->GetThemeFolder(); if (p) Folders.Add(p); } Folders.Add(ScribeResourcePath()); for (auto p: Folders) { LDirectory Dir; LgiTrace("%s:%i - Loading resource folder '%s'\n", _FL, p.Get()); for (auto b = Dir.First(p); b; b = Dir.Next()) { if (Dir.IsDir()) continue; auto Name = Dir.GetName(); if (MatchStr("Toolbar-*.png", Name)) { if (!d->ResFiles.Find(ResToolbarFile)) d->ResFiles.Add(ResToolbarFile, Dir.FullPath()); } else if (MatchStr("xgate-icons-*.png", Name)) d->ResFiles.Add(ResToolbarFile, Dir.FullPath()); else if (MatchStr("Icons-*.png", Name)) d->ResFiles.Add(ResIconsFile, Dir.FullPath()); } } ToolbarImgs.Reset(LLoadImageList(GetResourceFile(ResToolbarFile))); ImageList.Reset(LLoadImageList(GetResourceFile(ResIconsFile))); if (!ImageList) LgiTrace("%s:%i - Failed to load toolbar image ('xgate-icons-32.png' or 'Toolbar-24.png')\n", _FL); } int ScribeWnd::GetEventHandle() { return d->AppWndHnd; } void ScribeWnd::OnCloseInstaller() { d->Bar = NULL; if (InThread()) { PourAll(); } else LAssert(0); } void ScribeWnd::OnInstall(CapsHash *Caps, bool Status) { } bool ScribeWnd::NeedsCapability(const char *Name, const char *Param) { #if DEBUG_CAPABILITIES LgiTrace("ScribeWnd::NeedsCapability(%s, %s)\n", Name, Param); #endif if (!InThread()) { #if DEBUG_CAPABILITIES LgiTrace("%s:%i - Posting M_NEEDS_CAP\n", _FL); #endif PostEvent(M_NEEDS_CAP, (LMessage::Param)NewStr(Name), (LMessage::Param)NewStr(Param)); } else { if (!Name) return false; if (d->MissingCaps.Find(Name)) { #if DEBUG_CAPABILITIES LgiTrace("%s:%i - Already in MissingCaps\n", _FL); #endif return true; } d->MissingCaps.Add(Name, true); LStringPipe MsgBuf(256); int i = 0; // const char *k; // for (bool b=d->MissingCaps.First(&k); b; b=d->MissingCaps.Next(&k), i++) for (auto k : d->MissingCaps) { MsgBuf.Print("%s%s", i?", ":"", k.key); } LVariant Actions; if (stristr(Name, "OpenSSL")) { MsgBuf.Print(LLoadString(IDS_ERROR_SERVER_CONNECT)); if (Param) MsgBuf.Print("\n%s", Param); Actions.Add(new LVariant(LLoadString(IDS_INSTALL))); } else if (stristr(Name, "Registry")) { MsgBuf.Print(LLoadString(IDS_ERROR_REG_WRITE)); Actions.Add(new LVariant(LLoadString(IDS_DONT_SHOW_AGAIN))); } else if (stristr(Name, "SpellingDictionary")) { MsgBuf.Print(LLoadString(IDS_ERROR_NEED_INSTALL), Param); Actions.Add(new LVariant(LLoadString(IDS_DOWNLOAD))); } Actions.Add(new LVariant(LLoadString(IDS_OK))); #if DEBUG_CAPABILITIES LgiTrace("%s:%i - Actions.Length()=%i, Bar=%p\n", _FL, Actions.Length(), d->Bar); #endif if (Actions.Length()) { LAutoString Msg(MsgBuf.NewStr()); // Check the script hook here... bool ShowInstallBar = true; LArray Callbacks; if (GetScriptCallbacks(LBeforeInstallBar, Callbacks)) { for (unsigned i=0; iCastInt32()) ShowInstallBar = false; else Msg.Reset(TheMsg.ReleaseStr()); } } } // Now create the capability install bar... if (!d->Bar && ShowInstallBar && Actions.Type == GV_LIST) { // FYI Capabilities are handled in ScribeWnd::StartAction. LArray Act; for (auto v : *Actions.Value.Lst) Act.Add(v->Str()); d->Bar = new MissingCapsBar(this, &d->MissingCaps, Msg, this, Act); AddView(d->Bar, 2); AttachChildren(); OnPosChange(); } } } return true; } LAutoString ScribeWnd::GetDataFolder() { LVariant v; GetOptions()->GetValue(OPT_IsPortableInstall, v); char p[MAX_PATH_LEN]; if (LGetSystemPath(v.CastInt32() ? LSP_APP_INSTALL : LSP_APP_ROOT, p, sizeof(p))) { if (!LDirExists(p)) FileDev->CreateFolder(p); return LAutoString(NewStr(p)); } else LgiTrace("%s:%i - LgiGetSystemPath failed (portable=%i).\n", _FL, v.CastInt32()); return LAutoString(); } LScriptEngine *ScribeWnd::GetScriptEngine() { return d->Engine; } LScriptCallback ScribeWnd::GetCallback(const char *CallbackMethodName) { LScriptCallback Cb; auto Cur = d->CurrentScript(); if (Cur && Cur->Code) { Cb.Script = Cur; Cb.Func = Cur->Code->GetMethod(CallbackMethodName); } if (!Cb.Func) { for (auto s: d->Scripts) { Cb.Script = s; if ((Cb.Func = s->Code->GetMethod(CallbackMethodName))) break; } } return Cb; } bool ScribeWnd::RegisterCallback(LScriptCallbackType Type, LScriptArguments &Args) { if (!d->CurrentScript()) { LgiTrace("%s:%i - No current script.\n", _FL); return false; } char *Fn = Args[1]->Str(); LScriptCallback Cb = GetCallback(Fn); if (!Cb.Func) { LgiTrace("%s:%i - No callback '%s'.\n", _FL, Fn); return false; } switch (Type) { case LToolsMenu: { char *Menu = Args[0]->Str(); auto Cur = d->CurrentScript(); if (!Menu || !Fn || !Cur) { LgiTrace("%s:%i - menu=%s, fn=%s.\n", _FL, Menu, Fn); return false; } LScriptCallback &c = Cur->Callbacks.New(); c = Cb; c.Type = Type; c.Param = d->NextToolMenuId; LMenuItem *Tools = GetMenu()->FindItem(IDM_TOOLS_MENU); auto ToolSub = Tools ? Tools->Sub() : 0; if (ToolSub) { if (d->NextToolMenuId == IDM_TOOL_SCRIPT_BASE) { ToolSub->AppendSeparator(); } ToolSub->AppendItem(Menu, c.Param, true); d->NextToolMenuId++; } break; } case LThingContextMenu: case LFolderContextMenu: case LThingUiToolbar: case LMailOnBeforeSend: case LMailOnAfterReceive: case LApplicationToolbar: case LBeforeInstallBar: case LInstallComponent: case LOnTimer: case LRenderMail: case LOnLoad: { auto Cur = d->CurrentScript(); LAssert(d->Scripts.HasItem(Cur)); LScriptCallback &c = Cur->Callbacks.New(); c = Cb; c.Type = Type; if (Args.Length() > 2) c.Data = *Args[2]; break; } default: { LAssert(!"Not a known callback type"); return false; } } return true; } bool ScribeWnd::GetScriptCallbacks(LScriptCallbackType Type, LArray &Callbacks) { for (auto s: d->Scripts) { for (auto &c: s->Callbacks) { if (c.Type == Type) Callbacks.Add(&c); } } return Callbacks.Length() > 0; } bool ScribeWnd::ExecuteScriptCallback(LScriptCallback &c, LScriptArguments &Args, bool ReturnArgs) { if (!c.Func || !c.Script) return false; // Setup LVirtualMachine Vm(d); d->CurrentScripts.Add(c.Script); // Call the method bool Status = Vm.ExecuteFunction( c.Script->Code, c.Func, Args, LScribeScript::Inst->GetLog(), ReturnArgs ? &Args : NULL) != ScriptError; // Cleanup d->CurrentScripts.PopLast(); return Status; } LStream *ScribeWnd::ShowScriptingConsole() { auto Item = Menu->FindItem(IDM_SCRIPTING_CONSOLE); if (Item) { Item->Checked(!Item->Checked()); LScribeScript::Inst->ShowScriptingWindow(Item->Checked()); LVariant v; GetOptions()->SetValue(OPT_ShowScriptConsole, v = Item->Checked()); } return LScribeScript::Inst->GetLog(); } LOptionsFile::PortableType ScribeWnd::GetPortableType() { return d->GetInstallMode(); } void ScribeWnd::RemoteContent_AddSender(const char *Addr, bool WhiteList) { if (!Addr) return; auto Opt = WhiteList ? OPT_RemoteContentWhiteList : OPT_RemoteContentBlackList; LVariant v; GetOptions()->GetValue(Opt, v); // Not an error if not there... auto existing = LString(v.Str()).SplitDelimit(" ,\r\n"); for (auto p: existing) { if (MatchStr(p, Addr)) { LgiTrace("%s:%i - '%s' is already in '%s'\n", _FL, Addr, Opt); return; // Already in list... } } existing.SetFixedLength(false); existing.Add(Addr); auto updated = LString("\n").Join(existing); GetOptions()->SetValue(Opt, v = updated.Get()); LgiTrace("%s:%i - Added '%s' to '%s'\n", _FL, Addr, Opt); d->RemoteContent_Init = false; } ScribeRemoteContent ScribeWnd::RemoteContent_GetSenderStatus(const char *Addr) { if (!d->RemoteContent_Init) { LVariant v; if (GetOptions()->GetValue(OPT_RemoteContentWhiteList, v)) d->RemoteWhiteLst = LString(v.Str()).SplitDelimit(" ,\r\n"); if (GetOptions()->GetValue(OPT_RemoteContentBlackList, v)) d->RemoteBlackLst = LString(v.Str()).SplitDelimit(" ,\r\n"); d->RemoteContent_Init = true; } for (auto p: d->RemoteWhiteLst) if (MatchStr(p, Addr)) return RemoteAlwaysLoad; for (auto p: d->RemoteBlackLst) if (MatchStr(p, Addr)) return RemoteNeverLoad; return RemoteDefault; } void ScribeWnd::RemoteContent_ClearCache() { d->RemoteWhiteLst.Empty(); d->RemoteBlackLst.Empty(); d->RemoteContent_Init = false; } void ScribeWnd::OnSpellerSettingChange() { // Kill the current thread d->SpellerThread.Reset(); // Setup the new thread LSpellCheck *t = GetSpellThread(); if (t) { // Trigger an install if needed t->Check(d->AppWndHnd, "thisisamispeltword", 0, 18); } } bool ScribeWnd::SetSpellThreadParams(LSpellCheck *Thread) { if (!Thread) return false; LVariant Lang, Dict; GetOptions()->GetValue(OPT_SpellCheckLanguage, Lang); GetOptions()->GetValue(OPT_SpellCheckDictionary, Dict); LAutoPtr Params(new LSpellCheck::Params); if (!Params) return false; Params->IsPortable = GetPortableType(); Params->OptionsPath = GetOptions()->GetFile(); Params->Lang = Lang.Str(); Params->Dict = Dict.Str(); Params->CapTarget = this; Thread->SetParams(Params); return true; } LSpellCheck *ScribeWnd::CreateSpellObject() { LVariant PrefAspell; GetOptions()->GetValue(OPT_PreferAspell, PrefAspell); LAutoPtr Obj; if (PrefAspell.CastInt32()) Obj = CreateAspellObject(); #if defined(MAC) if (!Obj) Obj = CreateAppleSpellCheck(); #elif defined(WINDOWS) LArray Ver; int Os = LGetOs(&Ver); if ( !Obj && (Os == LGI_OS_WIN32 || Os == LGI_OS_WIN64) && ( Ver.Length() > 1 && ( Ver[0] > 6 || (Ver[0] == 6 && Ver[1] > 1) ) ) ) Obj = CreateWindowsSpellCheck(); #endif if (!Obj) Obj = CreateAspellObject(); SetSpellThreadParams(Obj); return Obj.Release(); } LSpellCheck *ScribeWnd::GetSpellThread(bool OverrideOpt) { LVariant Use; if (OverrideOpt) Use = true; else GetOptions()->GetValue(OPT_SpellCheck, Use); #if USE_SPELLCHECKER if ((Use.CastInt32() != 0) ^ (d->SpellerThread.Get() != 0)) d->SpellerThread.Reset(Use.CastInt32() ? CreateSpellObject() : NULL); #endif return d->SpellerThread; } LAutoString ScribeWnd::GetHttpProxy() { LAutoString Proxy; LVariant v; if (GetOptions()->GetValue(OPT_HttpProxy, v) && ValidStr(v.Str())) { Proxy.Reset(v.ReleaseStr()); } else { LProxyUri p; if (p.sHost) Proxy.Reset(NewStr(p.ToString())); } return Proxy; } InstallProgress *ScribeWnd::StartAction(MissingCapsBar *Bar, LCapabilityTarget::CapsHash *Components, const char *ActionParam) { if (!ActionParam) { LgiTrace("%s:%i - No action supplied.\n", _FL); return NULL; } LArray Callbacks; LVariant Action(ActionParam); if (GetScriptCallbacks(LInstallComponent, Callbacks)) { bool StartInstall = true; for (unsigned i=0; iCastInt32()) StartInstall = false; } } if (!Action.Str()) { LgiTrace("%s:%i - GInstallComponent removed action name.\n", _FL); return NULL; } if (!StartInstall) { LgiTrace("%s:%i - GInstallComponent script canceled install of '%s'.\n", _FL, ActionParam); return NULL; } } if (!_stricmp(Action.Str(), LLoadString(IDS_OK))) { // Do nothing d->MissingCaps.Empty(); } else if (!_stricmp(Action.Str(), LLoadString(IDS_DONT_SHOW_AGAIN))) { // Turn off registering as a client. LVariant No(false); GetOptions()->SetValue(OPT_RegisterWindowsClient, No); GetOptions()->SetValue(OPT_CheckDefaultEmail, No); } else if (!_stricmp(Action.Str(), LLoadString(IDS_INSTALL))) { #ifdef WINDOWS bool IsSsl = false; for (auto c: *Components) { if (!_stricmp(c.key, "openssl")) { IsSsl = true; break; } } if (IsSsl) { LString s; s.Printf(LLoadString(IDS_WINDOWS_SSL_INSTALL), LGetOsName()); auto q = new LAlert(this, AppName, s, "Open Website", LLoadString(IDS_CANCEL)); q->DoModal([this, q](auto dlg, auto id) { switch (id) { case 1: LExecute("https://slproweb.com/products/Win32OpenSSL.html"); break; default: break; } delete dlg; }); return NULL; } #endif return CapabilityInstaller::StartAction(Bar, Components, Action.Str()); } else if (!_stricmp(Action.Str(), LLoadString(IDS_SHOW_CONSOLE))) { ShowScriptingConsole(); } else if (!_stricmp(Action.Str(), LLoadString(IDS_OPEN_SOURCE))) { if (d->ErrSource) LExecute(d->ErrSource); else if (d->ErrFilter) d->ErrFilter->DoUI(); d->ErrSource.Empty(); d->ErrFilter = NULL; } else if ( !Stricmp(Action.Str(), LLoadString(IDS_SHOW_REMOTE_CONTENT)) || !Stricmp(Action.Str(), LLoadString(IDS_ALWAYS_SHOW_REMOTE_CONTENT))) { auto c = Components->begin(); LWindow *w = Bar->GetWindow(); if ((*c).key && !Stricmp((*c).key, "RemoteContent") && w) { LVariant Ret, Always(!Stricmp(Action.Str(), LLoadString(IDS_ALWAYS_SHOW_REMOTE_CONTENT))); LArray Args; Args[0] = &Always; w->CallMethod(DomToStr(SdShowRemoteContent), &Ret, Args); } } else if (!_stricmp(Action.Str(), LLoadString(IDS_DOWNLOAD))) { auto t = GetSpellThread(); if (t) t->InstallDictionary(); else LgiTrace("%s:%i - No spell thread.\n", _FL); } else LAssert(!"Unknown action."); return NULL; } HttpImageThread *ScribeWnd::GetImageLoader() { if (!d->ImageLoader) { LAutoString Proxy = GetHttpProxy(); d->ImageLoader = new HttpImageThread(this, Proxy, 0); } return d->ImageLoader; } char *ScribeWnd::GetUiTags() { if (!d->UiTags) { char UiTags[256] = "inscribe" #if defined WIN32 " win32" #elif defined LINUX " linux" #elif defined MAC " mac" #endif ; LVariant Tags; if (!GetOptions()) { LAssert(!"Where is the options?"); } else if (GetOptions()->GetValue("tags", Tags)) { size_t Len = strlen(UiTags); sprintf_s(UiTags+Len, sizeof(UiTags)-Len, " %s", Tags.Str()); } d->UiTags.Reset(NewStr(UiTags)); } return d->UiTags; } void ScribeWnd::OnCreate() { LgiTrace("ScribeWnd::OnCreate. ScribeState=%i\n", ScribeState); if (IsAttached() && ScribeState == ScribeConstructed) { ScribeState = ScribeInitializing; Construct3(); } } ScribeAccount *ScribeWnd::GetAccountByEmail(const char *Email) { if (!Email) return NULL; for (auto a : *GetAccounts()) { LVariant e = a->Identity.Email(); if (e.Str() && !_stricmp(e.Str(), Email)) { return a; } } return 0; } ScribeAccount *ScribeWnd::GetAccountById(int Id) { for (auto a : *GetAccounts()) { if (a->Receive.Id() == Id) { return a; } } return 0; } const char *ScribeWnd::EditCtrlMimeType() { LVariant Html; GetOptions()->GetValue(OPT_EditControl, Html); return Html.CastInt32() ? sTextHtml : sTextPlain; } LAutoString ScribeWnd::GetReplyXml(const char *MimeType) { bool IsHtml = MimeType && !_stricmp(MimeType, sTextHtml); LVariant s; GetOptions()->GetValue(IsHtml ? OPT_HtmlReplyFormat : OPT_TextReplyFormat, s); return LAutoString(s.ReleaseStr()); } LAutoString ScribeWnd::GetForwardXml(const char *MimeType) { bool IsHtml = MimeType && !_stricmp(MimeType, sTextHtml); LVariant s; GetOptions()->GetValue(IsHtml ? OPT_HtmlForwardFormat : OPT_TextForwardFormat, s); return LAutoString(s.ReleaseStr()); } LVmDebuggerCallback *ScribeWnd::GetDebuggerCallback() { return d; } GpgConnector *ScribeWnd::GetGpgConnector() { if (!d->GpgInst) { if (!GpgConnector::IsInstalled()) return NULL; d->GpgInst.Reset(new GpgConnector()); } return d->GpgInst; } LFont *ScribeWnd::GetPreviewFont() { return d->PreviewFont; } bool ScribeWnd::IsValid() { #if 0 try { for (ScribeAccount *a = Accounts.First(); a; a = Accounts.Next()) { } } catch(...) { return false; } #endif return true; } bool ScribeWnd::ShutdownIpc() { // Remove our instance from the shared memory if (ScribeIpc && ScribeIpc->GetPtr()) { ScribeIpcInstance *InstLst = (ScribeIpcInstance*) ScribeIpc->GetPtr(); if (ThisInst) memset(ThisInst, 0, sizeof(*ThisInst)); int c = 0; for (int i=0; iDestroy(); } ThisInst = 0; DeleteObj(ScribeIpc); return true; } bool ScribeWnd::GetVariant(const char *Name, LVariant &Value, const char *Array) { ScribeDomType Fld = StrToDom(Name); switch (Fld) { case SdQuote: // Type: String { return GetOptions()->GetValue(OPT_QuoteReplyStr, Value); } case SdName: // Type: String { Value = AppName; break; } case SdHome: // Type: String { Value = LGetExePath().Get(); break; } case SdNow: // Type: String { LDateTime Now; Now.SetNow(); char s[64]; Now.Get(s, sizeof(s)); Value = s; break; } case SdFolder: // Type: ScribeFolder[] // Pass system folder index or string as array parameter. { ScribeFolder *f = 0; if (!Array) return false; if (IsDigit(*Array)) { f = GetFolder(atoi(Array)); } else { f = GetFolder(Array); } if (!f) return false; Value = (LDom*)f; break; } case SdAppName: // Type: String { Value = AppName; break; } case SdCalendarToday: // Type: String { return Calendar::SummaryOfToday(this, Value); } case SdInboxSummary: { LStringPipe p; // Iterate through the mail stores for (auto m: Folders) { if (!m.Store) continue; auto Inbox = m.Store->GetObj(FIELD_INBOX); if (!Inbox) continue; auto Unread = Inbox->GetInt(FIELD_UNREAD); auto Name = Inbox->GetStr(FIELD_FOLDER_NAME); LString Path; Path.Printf("/%s/%s", m.Name.Get(), Name); if (Unread) p.Print("%s: %i unread
", m.Name.Get(), Path.Get(), Unread); else p.Print("%s: 0 unread
", m.Name.Get()); } // And also the IMAP full folders for (auto a: Accounts) { ScribeProtocol Protocol = a->Receive.ProtocolType(); if (Protocol == ProtocolImapFull) { LDataStoreI *Store = a->Receive.GetDataStore(); if (Store) { auto Inbox = Store->GetObj(FIELD_INBOX); if (Inbox) { auto Unread = Inbox->GetInt(FIELD_UNREAD); auto Name = Inbox->GetStr(FIELD_FOLDER_NAME); auto m = a->Receive.Name(); if (m.Str()) { LString Path; Path.Printf("/%s/%s", m.Str(), Name); if (Unread) p.Print("%s: %i unread
", m.Str(), Path.Get(), Unread); else p.Print("%s: 0 unread
", m.Str()); } } } } } Value = p.NewGStr().Get(); break; } case SdExecute: // Type: String { if (!Array) return false; const char *s = Array; char *Exe = LTokStr(s); if (!Exe) return false; while (*s && *s == ' ') s++; LStringPipe Out; LSubProcess p(Exe, (char*)s); if (p.Start()) { p.Communicate(&Out); LAutoString o(Out.NewStr()); LAutoString t(TrimStr(o)); Value = t; } DeleteArray(Exe); break; } case SdBuildType: // Type: String { #ifdef _DEBUG Value = "Debug"; #else Value = "Release"; #endif break; } case SdPlatform: // Type: String { LArray Ver; LGetOs(&Ver); LString::Array Va; for (auto i: Ver) Va.New().Printf("%i", i); #if defined __GTK_H__ auto Api = "GTK3"; #elif LGI_SDL auto Api = "SDL"; #elif LGI_COCOA auto Api = "Cocoa"; #elif LGI_CARBON auto Api = "Carbon"; #elif defined WIN32 auto Api = "WinApi"; #else #error "Impl me." auto Api = "#err"; #endif LString s; s.Printf("%s, v%s, %s", LGetOsName(), LString(".").Join(Va).Get(), Api); Value = s.Get(); break; } case SdVersion: // Type: String { char Ver[32]; sprintf_s(Ver, sizeof(Ver), "v%s", ScribeVer); Value = Ver; break; } case SdBuild: // Type: String { char s[128]; sprintf_s(s, sizeof(s), "%s, %s, %ibit", __DATE__, __TIME__, (int)(sizeof(NativeInt)*8)); Value = s; break; } case SdLanguage: // Type: String { LLanguage *l = LGetLanguageId(); if (!l) return false; char s[256]; sprintf_s(s, sizeof(s), "%s \"%s\"", l->Name, l->Id); Value = s; break; } case SdString: // Type: String { if (!Array) { LAssert(!"Missing string ID"); return false; } int Id = atoi(Array); Value = LLoadString(Id); break; } case SdCurrentFolder: // Type: ScribeFolder { Value = (LDom*) GetCurrentFolder(); break; } case SdView: // Type: LView { Value = (LView*)this; break; } case SdNoContact: // Type: Contact { Value = (NoContactType*)d->NoContact; break; } case SdAccounts: { if (Array) { if (IsDigit(*Array)) { auto i = atoi(Array); if (i >= 0 && i < (ssize_t)Accounts.Length()) { Value = (LDom*)Accounts[i]; } else return false; } else { for (auto a : Accounts) { LVariant nm = a->Send.Name(); if (nm.Str() && !_stricmp(nm.Str(), Array)) { Value = (LDom*)a; break; } } } } else { Value = (int32)Accounts.Length(); } break; } case SdOptions: { Value = GetOptions(); break; } case SdMailStorePaths: { if (!Value.SetList()) return false; for (auto Ms : Folders) Value.Add(new LVariant(Ms.Path)); break; } case SdRootFolders: { if (!Value.SetList() || !Tree) return false; for (auto *i = Tree->GetChild(); i; i = i->GetNext()) { ScribeFolder *c = dynamic_cast(i); if (c) { auto p = c->GetPath(); Value.Add(new LVariant(p)); } } break; } default: { return false; } } return true; } bool ScribeWnd::CallMethod(const char *MethodName, LVariant *ReturnValue, LArray &Args) { ScribeDomType m = StrToDom(MethodName); switch (m) { case SdGrowlOnMail: // Type: (Mail Obj) { if (Args.Length() != 1) { LgiTrace("%s:%i - Wrong arg count: %i.\n", _FL, (int)Args.Length()); return false; } Mail *m = dynamic_cast(Args[0]->CastDom()); if (!m) { LgiTrace("%s:%i - Invalid object.\n", _FL); return false; } GrowlOnMail(m); break; } case SdGrowlInfo: { auto Title = Args.Length() > 0 ? Args[0] : NULL; auto Text = Args.Length() > 1 ? Args[1] : NULL; GrowlInfo(Title ? Title->Str() : NULL, Text ? Text->Str() : NULL); break; } case SdGetClipboardText: // Type: () { LClipBoard c(this); *ReturnValue = c.Text(); break; } case SdSetClipboardText: // Type: (String Text) { if (Args.Length() != 1) { LgiTrace("%s:%i - Wrong arg count: %i.\n", _FL, (int)Args.Length()); return false; } char *Str = Args[0]->CastString(); LClipBoard c(this); if (ValidStr(Str)) *ReturnValue = c.Text(Str); else *ReturnValue = c.Empty(); break; } case SdLookupContactGroup: // Type: (String GroupName) { if (Args.Length() != 1) { LgiTrace("%s:%i - Wrong arg count: %i.\n", _FL, (int)Args.Length()); return false; } ContactGroup *Grp = LookupContactGroup(this, Args[0]->Str()); *ReturnValue = dynamic_cast(Grp); break; } case SdGetUserString: // Type: (LView ParentView, String PromptMessage[, Bool ObsurePassword[, String DefaultValue]]) { if (Args.Length() < 2) return false; LView *View = Args[0]->CastView(); char *Prompt = Args[1]->CastString(); bool Pass = Args.Length() > 2 ? Args[2]->CastInt32() != 0 : false; char *Default = Args.Length() > 3 ? Args[3]->CastString() : NULL; LString Result; bool Loop = true; auto i = new LInput(View ? View : this, Default, Prompt, AppName, Pass); i->DoModal([&Result, &Loop, i](auto dlg, auto id) { if (id) Result = i->GetStr(); delete dlg; Loop = false; }); // This is obviously not ideal, but I don't want to implement a scripting language callback for // something that should be a simple modal dialog that waits for user input. while (Loop) { LSleep(10); LYield(); } if (ReturnValue) *ReturnValue = Result; break; } case SdCreateAccount: // Type: () { ScribeAccount *a = new ScribeAccount(this, (int)Accounts.Length()); if (a) { if (ReturnValue) *ReturnValue = (LDom*)a; Accounts.Insert(a); a->Create(); } else return false; break; } case SdDeleteAccount: // Type: (ScribeAccount AccountToDelete) { if (Args.Length() != 1) return false; ScribeAccount *a = dynamic_cast(Args[0]->CastDom()); if (!a) { if (ReturnValue) *ReturnValue = false; } else { int Idx = (int)Accounts.IndexOf(a); if (Idx < 0 || a->IsOnline()) { if (ReturnValue) *ReturnValue = false; } else { Accounts.Delete(a); a->Delete(); delete a; // delete actual account object // Reindex remaining items so their are no gaps int i=0; auto it = Accounts.begin(); for (a = *it; a; a = *++it, i++) { a->ReIndex(i); } } } break; } case SdShowRemoteContent: { if (PreviewPanel) return PreviewPanel->CallMethod(MethodName, ReturnValue, Args); else return false; break; } case SdSearchHtml: // Type(Html, SearchExp, ResultExp) { if (Args.Length() != 3) { LgiTrace("%s:%i - SearchHtml requires 3 arguments.\n", _FL); *ReturnValue = false; return true; } auto Html = Args[0]->Str(); auto SearchExp = Args[1]->Str(); auto ResultExp = Args[2]->Str(); if (!Html || !SearchExp || !ResultExp) { LgiTrace("%s:%i - SearchHtml got non-string argument.\n", _FL); *ReturnValue = false; return true; } SearchHtml(ReturnValue, Html, SearchExp, ResultExp); return true; } case SdGetUri: // Type(UriToDownload, CallbackName) { if (Args.Length() < 2) { LgiTrace("%s:%i - GetUri requires at least 2 arguments.\n", _FL); *ReturnValue = false; return true; } auto Uri = Args[0]->Str(); auto Callback = Args[1]->Str(); LVariant *UserData = Args.Length() > 2 ? Args[2] : NULL; new ScriptDownloadContentThread(this, Uri, Callback, UserData); *ReturnValue = true; return true; } default: { LAssert(!"Unsupported method."); return false; } } return true; } LOptionsFile *ScribeWnd::GetOptions(bool Create) { if (!d->Options && Create) { LAssert(!"Not here... do it in LoadOptions."); return NULL; } return d->Options; } int OptionsFileCmp(OptionsInfo *a, OptionsInfo *b) { int64 Diff = b->Mod - a->Mod; return Diff < 0 ? -1 : (Diff > 0 ? 1 : 0); } OptionsInfo::OptionsInfo() { Score = 0; Mod = 0; Leaf = NULL; Usual = false; } OptionsInfo &OptionsInfo::operator =(char *p) { File = p; Leaf = LGetLeaf(File); if (Leaf) { char n[64]; sprintf_s(n, sizeof(n), "%s.xml", OptionsFileName); Usual = !_stricmp(n, Leaf); } return *this; } LOptionsFile *OptionsInfo::Load() { // Read the file... LAutoPtr Of(new LOptionsFile(File)); if (!Of) return NULL; if (!Of->SerializeFile(false)) return NULL; // Sanity check the options... LXmlTag *Acc = Of->LockTag(OPT_Accounts, _FL); if (!Acc) return NULL; Of->Unlock(); LXmlTag *Stores = Of->LockTag(OPT_MailStores, _FL); if (!Stores) return NULL; auto Count = Stores->Children.Length(); Of->Unlock(); if (Count == 0) Of.Reset(); return Of.Release(); } #define DEBUG_OPTS_SCAN 0 bool ScribeWnd::ScanForOptionsFiles(LArray &Files, LSystemPath PathType) { LString Root = LGetSystemPath(PathType); LDirectory Dir; char p[MAX_PATH_LEN]; if (IsUnitTest) Root += ".unittest"; #if DEBUG_OPTS_SCAN LgiTrace("%s:%i - Root='%s'\n", _FL, Root.Get()); #endif for (int b = Dir.First(Root); b; b = Dir.Next()) { if ( !Dir.IsDir() && Dir.Path(p, sizeof(p)) ) { LResolveShortcut(p, p, sizeof(p)); char *Ext = LGetExtension(Dir.GetName()); if (stristr(Dir.GetName(), OptionsFileName) != NULL && Ext && (!_stricmp(Ext, "xml") || !_stricmp(Ext, "bak"))) { OptionsInfo &i = Files.New(); i = p; i.Mod = Dir.GetLastWriteTime(); #if DEBUG_OPTS_SCAN LgiTrace("%s:%i - File='%s'\n", _FL, p); #endif } } } Files.Sort(OptionsFileCmp); // Scan through the results and pick out the normal file for (unsigned i=0; iOptions; i++) { if (Files[i].Usual) { d->Options = Files[i].Load(); #if DEBUG_OPTS_SCAN LgiTrace("%s:%i - Attempt '%s' = %p\n", _FL, Files[i].File.Get(), d->Options); #endif } } if (!d->Options) { // Scan through the alternative files and look for something // we can use. #if DEBUG_OPTS_SCAN LgiTrace("%s:%i - Scanning backups\n", _FL); #endif for (unsigned i=0; iOptions; i++) { if (!Files[i].Usual) { d->Options = Files[i].Load(); if (d->Options) { // Lets rename this baby back to the real filename LString Xml = OptionsFileName; Xml += ".xml"; LFile::Path Normal(Root, Xml); if (LFileExists(Normal)) FileDev->Delete(Normal); d->Options->SetFile(Normal); d->Options->SerializeFile(true); // sets to clean after changing filename. } } } } if (d->Options) { // Load OK: Clear out any old options files... #if DEBUG_OPTS_SCAN LgiTrace("%s:%i - Files.len=" LPrintfSizeT "\n", _FL, Files.Length()); #endif while (Files.Length() > 6) { auto Idx = Files.Length() - 1; auto &f = Files[Idx]; #if DEBUG_OPTS_SCAN LgiTrace("%s:%i - Delete '%s'\n", _FL, f.File.Get()); #endif FileDev->Delete(f.File); Files.DeleteAt(Idx); } } return d->Options != NULL; } bool ScribeWnd::IsUnitTest = false; bool ScribeWnd::LoadOptions() { bool Load = false; // Check if we are running unit tests... if ((IsUnitTest = LAppInst->GetOption("unittest"))) { d->UnitTestServer.Reset(new LUnitTestServer(this)); } // Look in the XGate folder #if WINNATIVE && !defined(_DEBUG) LRegKey xgate(false, "HKEY_CURRENT_USER\\Software\\XGate"); if (xgate.IsOk()) { char *Spool = xgate.GetStr("SpoolDir"); if (LDirExists(Spool)) { char File[MAX_PATH_LEN]; LMakePath(File, sizeof(File), Spool, "spool"); LMakePath(File, sizeof(File), File, OptionsFileName); strcat_s(File, sizeof(File), ".xml"); if (LFileExists(File)) { d->SetInstallMode(LOptionsFile::DesktopMode); LgiTrace("Selecting xgate mode based on options file path.\n"); d->Options = new LOptionsFile(File); } } } #endif // Now look in the application install folder LArray Files; if (!d->Options && ScanForOptionsFiles(Files, LSP_APP_INSTALL)) { // File is in the install folder... d->SetInstallMode(LOptionsFile::PortableMode); LgiTrace("Selecting portable mode based on options file path.\n"); } // Look in the app root if ( !d->Options && ScanForOptionsFiles(Files, LSP_APP_ROOT) ) { // Desktop mode d->SetInstallMode(LOptionsFile::DesktopMode); LgiTrace("Selecting desktop mode based on options file path.\n"); } // Do multi-instance stuff if (d->Options && d->Options->GetFile()) { // Search for other instances of Scribe if (ScribeIpc) { int i; ScribeIpcInstance *InstLst = (ScribeIpcInstance*) ScribeIpc->GetPtr(); ScribeIpcInstance *Mul = 0; for (i=0; iMulPassword = Mul->Password; break; } } for (i=0; iIsScribe(), InstLst->Magic, InstLst->Pid); if (InstLst->IsScribe() && InstLst != ThisInst) { // LgiTrace("%s, %s\n", InstLst->OptionsPath, d->Options->GetFile()); if (Mul || _stricmp(InstLst->OptionsPath, d->Options->GetFile()) == 0) { int Pid = InstLst->Pid; OsAppArguments *Args = LAppInst->GetAppArgs(); if (!LIsProcess(Pid)) { continue; } char *Utf8 = 0; #if WINNATIVE Utf8 = WideToUtf8(Args->lpCmdLine); #else LStringPipe p; for (int i=1; iArgs; i++) { if (i > 1) p.Push(" "); char *Sp = strchr(Args->Arg[i], ' '); if (Sp) p.Push("\""); p.Push(Args->Arg[i]); if (Sp) p.Push("\""); } Utf8 = p.NewStr(); #endif if (Utf8) { size_t Len = strlen(Utf8); if (Len > 255) { InstLst->Flags |= SCRIBE_IPC_LONG_ARGS; InstLst->Flags &= ~SCRIBE_IPC_CONTINUE_ARGS; for (char *u = Utf8; Len > 0; u += 255) { ssize_t Part = MIN(sizeof(InstLst->Args)-1, Len); memcpy(InstLst->Args, u, Part); Len -= Part; int64 Start = LCurrentTime(); while (LCurrentTime() - Start < 60000) { if (TestFlag(InstLst->Flags, SCRIBE_IPC_CONTINUE_ARGS) && InstLst->Args[0] == 0) { Start = 0; break; } LSleep(10); } if (Start) { LgiTrace("%s:%i - SendLA timed out.\n", _FL); break; } } InstLst->Flags &= ~(SCRIBE_IPC_CONTINUE_ARGS | SCRIBE_IPC_LONG_ARGS); } else { strcpy_s(InstLst->Args, sizeof(InstLst->Args), Utf8); } DeleteArray(Utf8); ShutdownIpc(); LgiTrace("Passed args to the other running instance of Scribe (pid=%i)\n", Pid); LCloseApp(); return false; } else LgiTrace("%s:%i - No arguments to pass.\n", _FL); } } } if (Mul && LFileExists(Mul->OptionsPath)) { // No instance of Scribe is running, but MUL may be keeping // the previously run instance around. So we should run that // by using the options file and password from MUL's instance // record. DeleteObj(d->Options); d->Options = new LOptionsFile(Mul->OptionsPath); } } // Insert ourselves into the instance list if (ThisInst) { strcpy_s(ThisInst->OptionsPath, sizeof(ThisInst->OptionsPath), d->Options->GetFile()); } } // Open file and load.. if (!Load && d->Options) { LOptionsFile *Opts = GetOptions(); Load = Opts->SerializeFile(false); if (Load) { LVariant v = d->GetInstallMode() == LOptionsFile::PortableMode; GetOptions()->SetValue(OPT_IsPortableInstall, v); } else { auto err = GetOptions()->GetError(); LgiMsg( this, LLoadString(IDS_ERROR_LR8_FAILURE), AppName, MB_OK, err); } } if (!d->Options) { // d->Options = new LOptionsFile(d->GetInstallMode(), OptionsFileName); return false; } if (d->Options) { LVariant v; if (!d->Options->GetValue(OPT_IsPortableInstall, v) && d->GetInstallMode() != LOptionsFile::UnknownMode) { v = d->GetInstallMode() == LOptionsFile::PortableMode; d->Options->SetValue(OPT_IsPortableInstall, v); } ScribeOptionsDefaults(d->Options); if (Load) { if (GetOptions()->GetValue(OPT_PrintSettings, v)) { auto *p = GetPrinter(); if (p) { LString s = v.Str(); p->Serialize(s, false); } } } if (d->Options->GetValue(OPT_PreviewLines, v)) { Mail::PreviewLines = v.CastInt32() != 0; } // upgrade smtp password const char *Pw = "SmtpPsw"; if (!GetOptions()->GetValue(OPT_EncryptedSmtpPassword, v)) { // no encrypted password, look for unencrypted password if (GetOptions()->GetValue(Pw, v)) { GPassword p; p.Set(v.Str()); p.Serialize(GetOptions(), OPT_EncryptedSmtpPassword, true); } } // if old un-encrypted password exists... // delete the key, we are now storing an encrypted // password if (GetOptions()->GetValue(Pw, v)) GetOptions()->DeleteValue(Pw); if (GetOptions()->GetValue(OPT_AdjustDateTz, v)) Mail::AdjustDateTz = !v.CastInt32(); if (!GetOptions()->GetValue(OPT_ConfirmDelete, v)) GetOptions()->SetValue(OPT_ConfirmDelete, v = true); if (!GetOptions()->GetValue(OPT_DelDirection, v)) GetOptions()->SetValue(OPT_DelDirection, v = DeleteActionPrev); if (GetOptions()->GetValue(OPT_SizeInKiB, v)) OptionSizeInKiB = v.CastInt32() != 0; if (GetOptions()->GetValue(OPT_RelativeDates, v)) ShowRelativeDates = v.CastInt32() != 0; // date format if (GetOptions()->GetValue(OPT_DateFormat, v)) { int Idx = v.CastInt32(); if (Idx >= 0 && Idx < CountOf(DateTimeFormats)) LDateTime::SetDefaultFormat(DateTimeFormats[Idx]); } // SSL debug logging if (GetOptions()->GetValue(OPT_DebugSSL, v)) SslSocket::DebugLogging = v.CastInt32() != 0; // Growl if (GetOptions()->GetValue(OPT_GrowlEnabled, v) && v.CastInt32()) { LVariant Ver, Bld; GetVariant(DomToStr(SdVersion), Ver); GetVariant("Build", Bld); LString n; n.Printf("%s\n%s", Ver.Str(), Bld.Str()); GrowlInfo("Scribe has started up...", n); } } #if LGI_EXCEPTIONS try { #endif // Default the font settings to the system font // if they don't already exist const char *OptFont[] = { OPT_EditorFont, OPT_PrintFont, OPT_HtmlFont, 0 }; int Index = 0; for (const char **Opt=OptFont; *Opt; Opt++, Index++) { LVariant v; if (!GetOptions()->GetValue(*Opt, v)) { LFontType Type; if (Type.GetSystemFont("System")) { if (Index == 2) { int Pt = Type.GetPointSize(); Type.SetPointSize(Pt+3); } Type.Serialize(GetOptions(), *Opt, true); } } } #if LGI_EXCEPTIONS } catch (...) { LgiMsg( this, LLoadString(IDS_ERROR_FONT_SETTINGS), AppName, MB_OK); } #endif return true; } bool ScribeWnd::SaveOptions() { LStringPipe Log(256); bool Status = false; bool WriteFailed = false; bool WndStateSet = false; RestartSave: if (d->Options && !d->Options->GetFile()) { bool PortableOk = true; char Path[MAX_PATH_LEN]; char Leaf[32]; sprintf_s(Leaf, sizeof(Leaf), "%s.xml", OptionsFileName); Log.Print("No current path for '%s', creating...\n", Leaf); LVariant v; GetOptions()->GetValue(OPT_IsPortableInstall, v); if (v.CastInt32()) { if (!LGetSystemPath(LSP_APP_INSTALL, Path, sizeof(Path))) { PortableOk = false; Log.Print("Error: LgiGetSystemPath(LSP_APP_INSTALL) failed.\n"); } else { LMakePath(Path, sizeof(Path), Path, Leaf); // Do write test to confirm we are good to go LFile f; if (f.Open(Path, O_WRITE)) { f.Close(); FileDev->Delete(Path, false); d->Options->SetFile(Path); } else { PortableOk = false; Log.Print("Warning: '%s' is not writable.\n", Path); } } } if (!v.CastInt32() || !PortableOk) { // Desktop mode then. if (v.CastInt32()) { const char *Msg = "Switching to desktop mode because the install folder is not writable."; Log.Print("%s\n", Msg); LgiMsg(this, Msg, AppName, MB_OK); GetOptions()->SetValue(OPT_IsPortableInstall, v = false); } if (!LGetSystemPath(LSP_APP_ROOT, Path, sizeof(Path))) { Log.Print("Error: LgiGetSystemPath(LSP_APP_ROOT) failed.\n"); } else { LMakePath(Path, sizeof(Path), Path, LAppInst->LBase::Name()); if (!LDirExists(Path)) { if (!FileDev->CreateFolder(Path)) { Log.Print("Error: CreateFolder('%s') failed.\n", Path); } } LMakePath(Path, sizeof(Path), Path, Leaf); // Do write test to confirm we are good to go LFile f; if (f.Open(Path, O_WRITE)) { f.Close(); FileDev->Delete(Path, false); d->Options->SetFile(Path); } else { Log.Print("Error: '%s' is not writable.\n", Path); } } } } if (d->Options && d->Options->GetFile() && d->Options->IsValid()) { // Backup options file char Backup[MAX_PATH_LEN]; strcpy_s(Backup, sizeof(Backup), d->Options->GetFile()); char *Ext = LGetExtension(Backup); if (Ext) { *--Ext = 0; LString s; for (int i=1; i<100; i++) { s.Printf("%s_%i.bak", Backup, i); if (!LFileExists(s)) break; } if (!LFileExists(s)) FileDev->Move(d->Options->GetFile(), s); } // Update some settings... #if LGI_VIEW_HANDLE if (Handle()) #endif WndStateSet = SerializeState(GetOptions(), OPT_ScribeWndPos, false); LVariant v; if (Splitter) GetOptions()->SetValue(OPT_SplitterPos, v = (int)Splitter->Value()); if (d->SubSplit) { auto First = d->SubSplit->GetViewAt(0); if (First == (LViewI*)SearchView) { auto Lst = (SearchView) ? d->SubSplit->GetViewAt(1) : NULL; if (Lst) GetOptions()->SetValue(OPT_SubSplitPos, v = (int)Lst->GetPos().Y()); } else GetOptions()->SetValue(OPT_SubSplitPos, v = (int)d->SubSplit->Value()); } // Write them... if (GetOptions()->SerializeFile(true)) { Status = true; } else { // We probably don't have write permissions to the install folder... Log.Print("Error: Options.Serialize failed.\n"); if (!WriteFailed) { // This blocks any possibility of an infinite loop WriteFailed = true; d->Options->SetFile(NULL); // Set desktop mode explicitly LVariant v; GetOptions()->GetValue(OPT_IsPortableInstall, v = false); Log.Print("Restarting save after setting desktop mode...\n"); goto RestartSave; } } } if (!Status) { LString a = Log.NewGStr(); LgiMsg(this, "Saving options failed:\n%s", AppName, MB_OK, a.Get()); } if (!WndStateSet) { LRect r(10, 10, 790, 590); SetPos(r); MoveToCenter(); } return Status; } // // Command Line Options: // // -m, -t : To recipient(s) // -f : The filename of the attachment // -b : Attach as a binary // -c : CC'd recipient(s) // -s : Subject for the email // -n : Send now... else UI is shown // -p : Print the file // -upgrade_folders : trigger a folder upgrade // -o : Load the following options file // -u : Load the following URL/file // void ScribeWnd::OnCommandLine() { // check command line args LString Str, File; bool CreateMail = false; CreateMail = LAppInst->GetOption("m", Str); if (!CreateMail) CreateMail = LAppInst->GetOption("t", Str); bool HasFile = LAppInst->GetOption("f", File); if (!CreateMail) CreateMail = HasFile; LString OpenArg; if (LAppInst->GetOption("u", OpenArg)) { LUri u(OpenArg); if (u.sProtocol) { OnUrl(OpenArg); } else if (LFileExists(OpenArg)) { LArray Files; Files.Add(OpenArg); OnReceiveFiles(Files); } } Mail *NewEmail = 0; if (CreateMail && Str) { // strip off quotes if needed char *In = Str, *Out = Str; for (; In && *In; In++) { if (!strchr("\'\"", *In)) { *Out++ = *In; } } *Out++ = 0; // create object NewEmail = dynamic_cast(CreateItem(MAGIC_MAIL, NULL, false)); if (NewEmail) { Mailto mt(this, Str); mt.Apply(NewEmail); // cc's? if (LAppInst->GetOption("c", Str)) { SetRecipients(this, Str, NewEmail->GetObject()->GetList(FIELD_TO), MAIL_ADDR_CC); } // attach a file? if (File) { if (LAppInst->GetOption("b")) { // attach as a binary file NewEmail->AttachFile(this, &File[0]); } else { // insert as the body LAutoString b(LReadTextFile(&File[0])); if (b) { NewEmail->SetBody(b); } } } // subject? if (LAppInst->GetOption("s", Str)) { NewEmail->SetSubject(Str); } // Send now or later? if (LAppInst->GetOption("n")) { // Check for exit after send option d->ExitAfterSend = LAppInst->GetOption("exit"); // now NewEmail->SetFlags(MAIL_CREATED | MAIL_READY_TO_SEND | NewEmail->GetFlags()); NewEmail->Save(); OnCommand(IDM_SEND_MAIL, 0, #ifndef __GTK_H__ Handle() #else NULL #endif ); } else { // later NewEmail->DoUI(); } } } // Pop3 on startup option LVariant n; if (GetOptions()->GetValue(OPT_Pop3OnStart, n) && n.CastInt32()) { OnCommand(IDM_RECEIVE_MAIL, 0, NULL); } } void ScribeWnd::SetCurrentIdentity(int i) { LVariant v = i; GetOptions()->SetValue(OPT_CurrentIdentity, v); if (DefaultIdentityItem) DefaultIdentityItem->Checked(i < 0); for (auto a: Accounts) { a->SetCheck(i == a->GetIndex()); } } ScribeAccount *ScribeWnd::GetCurrentAccount() { auto Idx = GetCurrentIdentity(); ScribeAccount *a = (Idx >= 0 && Idx < (ssize_t)Accounts.Length()) ? Accounts.ItemAt(Idx) : NULL; bool ValidId = a != NULL && a->IsValid(); if (!ValidId) { LAssert(!"No current identity?"); // Find a valid account to be the identity... for (auto a : Accounts) { if (!a->Send.Disabled() && a->Identity.IsValid()) { break; } } } return a; } int ScribeWnd::GetCurrentIdentity() { LVariant i; if (GetOptions()->GetValue(OPT_CurrentIdentity, i)) return i.CastInt32(); else if (ScribeState != ScribeInitializing) LgiTrace("%s:%i - No OPT_CurrentIdentity set.\n", _FL); return -1; } void ScribeWnd::SetupAccounts() { int i, CurrentIdentity = GetCurrentIdentity(); if (StatusPanel) { StatusPanel->Empty(); } #if !defined(COCOA) // FIXME LAssert(ReceiveMenu && PreviewMenu); #endif if (SendMenu) SendMenu->Empty(); if (ReceiveMenu) ReceiveMenu->Empty(); if (PreviewMenu) PreviewMenu->Empty(); if (IdentityMenu) { IdentityMenu->Empty(); } static bool Startup = true; bool ResetDefault = false; LArray Enabled; for (i=0; true; i++) { // char *s = 0; ScribeAccount *a = Startup ? new ScribeAccount(this, i) : Accounts[i]; if (a) { if (i == 0) { a->Create(); } a->Register(this); LVariant ReceiveName = a->Receive.Name(); LVariant ReceiveServer = a->Receive.Server(); LVariant SendServer = a->Send.Server(); if (i == 0 || ValidStr(ReceiveName.Str()) || ValidStr(ReceiveServer.Str()) || ValidStr(SendServer.Str()) ) { a->Send.SendItem = SendItem; a->Receive.ReceiveItem = ReceiveItem; a->Receive.PreviewItem = PreviewItem; if (!Accounts.HasItem(a)) { Accounts.Insert(a); } if (i) a->Create(); a->InitMenus(); // Identity Menu Item LVariant IdEmail = a->Identity.Email(); LVariant IdName = a->Identity.Name(); if (IdentityMenu && ValidStr(IdEmail.Str())) { char s[256]; if (IdName.Str()) sprintf_s(s, sizeof(s), "%s <%s>", IdName.Str(), IdEmail.Str()); else sprintf_s(s, sizeof(s), "<%s>", IdEmail.Str()); a->SetMenuItem(IdentityMenu->AppendItem(s, IDM_IDENTITY_BASE+i+1, !a->Send.Disabled())); if (a->Send.Disabled()) { a->SetCheck(false); if (i == CurrentIdentity) ResetDefault = true; } else { a->SetCheck(i == CurrentIdentity); Enabled[i] = a; } } } else { Accounts.Delete(a); DeleteObj(a); } } if (!a) break; } if ((ResetDefault || CurrentIdentity < 0) && Enabled.Length()) { for (unsigned i=0; iSetCheck(true); LVariant v; GetOptions()->SetValue(OPT_CurrentIdentity, v = (int)i); break; } } } Startup = false; if (ReceiveMenu && i == 0) { ReceiveMenu->AppendItem(LLoadString(IDS_NO_ITEMS), 0, false); } if (StatusPanel) { StatusPanel->OnAccountListChange(); } SetPulse(100); } ////////////////////////////////////////////////////////////////////////////// class LShutdown : public LDialog { LTextLabel *Msg; LButton *KillBtn; LButton *CancelBtn; bool Disconnected; public: ScribeAccount *Wait; List *Accounts; LShutdown(List *accounts) { Wait = 0; Disconnected = false; Accounts = accounts; LRect r( 0, 0, 320 + LAppInst->GetMetric(LGI_MET_DECOR_X), 70 + LAppInst->GetMetric(LGI_MET_DECOR_Y)); SetPos(r); MoveToCenter(); char Str[256]; sprintf_s(Str, sizeof(Str), "%s exiting...", AppName); LView::Name(Str); AddView(Msg = new LTextLabel(-1, 10, 10, 300, -1, "None")); AddView(KillBtn = new LButton(IDC_KILL, 70, 35, 60, 20, "Kill")); AddView(CancelBtn = new LButton(IDCANCEL, 140, 35, 60, 20, "Cancel")); if (KillBtn) { KillBtn->Enabled(false); } } void OnCreate() { SetPulse(100); } void OnPulse() { if (Accounts) { if (!Wait) { Wait = (*Accounts)[0]; if (Wait) { Disconnected = false; char s[256]; LVariant v = Wait->Receive.Name(); sprintf_s(s, sizeof(s), "Waiting for '%s'", v.Str() ? v.Str() : (char*)"Untitled..."); Msg->Name(s); Accounts->Delete(Wait); Wait->Stop(); KillBtn->Enabled(true); } else { SetPulse(); EndModal(true); } } if (Wait && !Wait->IsOnline()) { Wait = 0; Msg->Name("None"); KillBtn->Enabled(false); } } else { SetPulse(); EndModal(false); } } int OnNotify(LViewI *Ctrl, LNotification n) { switch (Ctrl->GetId()) { case IDC_KILL: { if (Wait) { if (!Disconnected) { Disconnected = true; Wait->Disconnect(); } else { Wait->Kill(); } } break; } case IDCANCEL: { EndModal(false); break; } } return 0; } }; bool ScribeWnd::OnRequestClose(bool OsShuttingDown) { if (FolderTasks.Length() > 0) { LgiTrace("%s:%i - %i folder tasks still busy...\n", _FL, FolderTasks.Length()); return false; } LString OnClose = LAppInst->GetConfig("Scribe.OnClose"); if (!d->IngoreOnClose && !OsShuttingDown && !Stricmp(OnClose.Get(), "minimize")) { SetZoom(LZoomMin); return false; } Visible(false); if (ScribeState != ScribeRunning) { // Inside a folder load/unload or initialization // Tell the loader to quit out... ScribeState = ScribeExiting; // Leave now, we can exit when we're ready return false; } else if (IsSending() || GetActiveThreads() > 0) { // whack up a shutdown window List Online; for (auto i: Accounts) { i->OnEndSession(); if (i->IsOnline()) { Online.Insert(i); } } auto Dlg = new LShutdown(&Online); Dlg->DoModal([this, Dlg](auto dlg, auto id) { if (id) { ScribeState = ScribeExiting; LCloseApp(); } else { ScribeState = ScribeRunning; Visible(true); } }); return false; // At the very minimum the app has to wait for the user to respond. } else { // End all sessions if any... for (auto i: Accounts) { i->OnEndSession(); } } // close all the other top level windows while (ThingUi::All.Length() > 0) { ThingUi *Ui = ThingUi::All.First(); if (!Ui->OnRequestClose(OsShuttingDown)) { ScribeState = ScribeRunning; Visible(true); return false; } size_t Start = ThingUi::All.Length(); Ui->Quit(); if (ThingUi::All.Length() >= Start) { LAssert(0); break; } } SerializeState(GetOptions(), OPT_ScribeWndPos, false); LCloseApp(); return LWindow::OnRequestClose(OsShuttingDown); } void ScribeWnd::DoOnTimer(LScriptCallback *c) { if (!c) return; auto Now = LCurrentTime(); if (c->PrevTs) { auto Since = Now - c->PrevTs; double Sec = (double)Since / 1000.0; if (Sec >= c->fParam) { // Call the function c->PrevTs = Now; LVirtualMachine Vm; LScriptArguments Args(&Vm); LVariant This((LDom*)this); Args.Add(&This); ExecuteScriptCallback(*c, Args); } } else { c->PrevTs = Now; } } void ScribeWnd::OnMinute() { if (Folders.Length() == 0) return; // Check for calendar event alarms... Calendar::CheckReminders(); // Check for any outgoing email that should be re-attempted... ScribeFolder *Outbox = GetFolder(FOLDER_OUTBOX); if (Outbox) { bool Resend = false; for (auto t : Outbox->Items) { Mail *m = t->IsMail(); if (m && !TestFlag(m->GetFlags(), MAIL_SENT) && TestFlag(m->GetFlags(), MAIL_READY_TO_SEND) && m->SendAttempts > 0) { Resend = true; break; } } if (Resend) Send(); } LArray Cb; if (GetScriptCallbacks(LOnTimer, Cb)) { for (auto c: Cb) { if (!c->Func) continue; if (c->fParam == 0.0) { // Work out the period from 'Data' char *s = c->Data.Str(); while (*s && IsWhiteSpace(*s)) s++; char *u = s; while (*u && !IsAlpha(*u)) u++; double v = atof(s); switch (*u) { case 's': case 'S': // seconds c->fParam = v; break; case 'm': case 'M': // mins c->fParam = v * LDateTime::MinuteLength; break; case 'h': case 'H': // hours c->fParam = v * LDateTime::HourLength; break; case 'd': case 'D': // days c->fParam = v * LDateTime::DayLength; break; default: { LgiTrace("%s:%i - Couldn't understand period '%s'\n", _FL, c->Data.Str()); c->Data.Empty(); break; } } if ((c->OnSecond = c->fParam < 60.0)) { d->OnSecondTimerCallbacks.Add(c); } } if (!c->OnSecond) DoOnTimer(c); } } } void ScribeWnd::OnHour() { // Force time zone update in case of daylight savings change. LDateTime::SystemTimeZone(true); // Check if we need should be doing a software update check static bool InSoftwareCheck = false; if (!InSoftwareCheck) { char s[64]; InSoftwareCheck = true; LVariant v; if (GetOptions()->GetValue(OPT_SoftwareUpdate, v) && v.CastInt32()) { LDateTime Now, Last; Now.SetFormat(GDTF_YEAR_MONTH_DAY); Last.SetFormat(Now.GetFormat()); Now.SetNow(); if (!GetOptions()->GetValue(OPT_SoftwareUpdateLast, v) || !Last.Set(v.Str())) { // Record now as the last check point Now.Get(s, sizeof(s)); GetOptions()->SetValue(OPT_SoftwareUpdateLast, v = s); } else if (GetOptions()->GetValue(OPT_SoftwareUpdateTime, v)) { // Valid last check date/time. switch (v.CastInt32()) { case 0: // Week Last.AddDays(7); break; case 1: // Month Last.AddMonths(1); break; case 2: // Year Last.AddMonths(12); break; default: LgiTrace("%s:%i - The option '%s' is not valid\n", _FL, OPT_SoftwareUpdateTime); return; } if (Last < Now) { // Save the last date for next time... Now.Get(s, sizeof(s)); GetOptions()->SetValue(OPT_SoftwareUpdateLast, v = s); // Check for update now... LSoftwareUpdate::UpdateInfo Info; GetOptions()->GetValue(OPT_SoftwareUpdateIncBeta, v); IsSoftwareUpToDate(Info, this, false, v.CastInt32() != 0, [Info, this](auto s) { if (s == SwOutOfDate) if (UpgradeSoftware(Info, this, true)) LCloseApp(); }); } } } InSoftwareCheck = false; } } bool ScribeWnd::SaveDirtyObjects(int TimeLimitMs) { bool Status = false; if (Thing::DirtyThings.Length() > 0) { static bool SavingObjects = false; if (!SavingObjects) { SavingObjects = true; LArray WriteTimes; // ssize_t StartDirty = Thing::DirtyThings.Length(); uint64 Start = LCurrentTime(); for (unsigned i=0; iSave(NULL)) { WriteTimes.Add((int) (LCurrentTime() - WriteStart)); LAssert(!ThingType::DirtyThings.HasItem(t)); Status = true; } else { LgiTrace("Failed to save thing type 0x%x\n", t->Type()); FailedWrites++; if (FailedWrites > 2) { while (ThingType::DirtyThings.Length()) ThingType::DirtyThings[0]->SetDirty(false); FailedWrites = 0; } } } } SavingObjects = false; /* if (Status) { LStringPipe p; p.Print("WriteTimes: "); for (unsigned i=0; iLastTs >= 1000) { d->LastTs = Now; OnPulseSecond(); } } } void ScribeWnd::OnPulseSecond() { #if PROFILE_ON_PULSE LProfile Prof("NewMailLst handling"); Prof.HideResultsIfBelow(50); #endif if (Mail::NewMailLst.Length() > 0) { LVariant Blink; if (GetOptions()->GetValue(OPT_BlinkNewMail, Blink) && Blink.CastInt32()) { TrayIcon.Value((TrayIcon.Value() == TRAY_ICON_MAIL) ? TRAY_ICON_NONE : TRAY_ICON_MAIL); } } else { bool Err = false; for (auto a: Accounts) { if (!a->Receive.GetStatus() || !a->Send.GetStatus()) { Err = true; } } TrayIcon.Value(Err ? TRAY_ICON_ERROR : TRAY_ICON_NORMAL); } #if PROFILE_ON_PULSE Prof.Add("StatusPanel handling"); #endif if (StatusPanel) { StatusPanel->OnPulse(); } #if PROFILE_ON_PULSE Prof.Add("OnXXXX handling"); #endif LDateTime Now; Now.SetNow(); if (d->LastMinute != Now.Minutes()) // Check every minute... { d->LastMinute = Now.Minutes(); OnMinute(); } if (d->LastHour != Now.Hours()) // Check every hour... { d->LastHour = Now.Hours(); OnHour(); } { // These timers need to be checked every second... for (auto c: d->OnSecondTimerCallbacks) DoOnTimer(c); } #if PROFILE_ON_PULSE Prof.Add("Instance handling"); #endif if (ThisInst && ValidStr(ThisInst->Args)) { LStringPipe p; p.Push(ThisInst->Args); if (ThisInst->Flags & SCRIBE_IPC_LONG_ARGS) { ThisInst->Flags |= SCRIBE_IPC_CONTINUE_ARGS; int64 Start = LCurrentTime(); while ( TestFlag(ThisInst->Flags, SCRIBE_IPC_LONG_ARGS) && LCurrentTime() - Start < 60000) { ZeroObj(ThisInst->Args); while ( TestFlag(ThisInst->Flags, SCRIBE_IPC_LONG_ARGS) && !ThisInst->Args[0] && LCurrentTime() - Start < 60000) { LSleep(10); } p.Push(ThisInst->Args); } } ZeroObj(ThisInst->Args); LAutoString Arg(p.NewStr()); if (Arg) { OsAppArguments AppArgs(0, 0); LgiTrace("Received cmd line: %s\n", Arg.Get()); AppArgs.Set(Arg); LAppInst->SetAppArgs(AppArgs); if (LAppInst->GetOption("m") && LAppInst->GetOption("f")) ; else LAppInst->OnCommandLine(); OnCommandLine(); if (GetZoom() == LZoomMin) SetZoom(LZoomNormal); Visible(true); } } #if PROFILE_ON_PULSE Prof.Add("PreviewPanel handling"); #endif if (PreviewPanel) { PreviewPanel->OnPulse(); } } void ScribeWnd::AddFolderToMru(char *FileName) { if (FileName) { // read MRU List Files; int i; for (i=0; i<10; i++) { char Key[32]; LVariant f; sprintf_s(Key, sizeof(Key), "FolderMru.%i", i); if (GetOptions()->GetValue(Key, f)) { Files.Insert(NewStr(f.Str())); GetOptions()->DeleteValue(Key); } } // remove FileName if present for (auto f: Files) { if (_stricmp(f, FileName) == 0) { Files.Delete(f); DeleteArray(f); break; } } // insert FileName at the start of the list Files.Insert(NewStr(FileName)); // write MRU for (i=0; i<10; i++) { char *n = Files.ItemAt(i); if (n) { char Key[32]; sprintf_s(Key, sizeof(Key), "FolderMru.%i", i); LVariant f; GetOptions()->SetValue(Key, f = n); } else break; } // Clean up Files.DeleteArrays(); } } bool ScribeWnd::CleanFolders(ScribeFolder *f) { if (!f) return false; if (f->Select()) { f->SerializeFieldWidths(); } for (ScribeFolder *c = f->GetChildFolder(); c; c = c->GetNextFolder()) { CleanFolders(c); } return true; } void ScribeWnd::OnFolderChanged(LDataFolderI *folder) { } bool ScribeWnd::OnFolderTask(LEventTargetI *Ptr, bool Add) { if (Add) { if (FolderTasks.HasItem(Ptr)) { LAssert(!"Can't add task twice."); return false; } FolderTasks.Add(Ptr); return true; } else { if (!FolderTasks.HasItem(Ptr)) { LAssert(!"Item not part of task list."); return false; } FolderTasks.Delete(Ptr); return true; } } LMailStore *ScribeWnd::GetDefaultMailStore() { LMailStore *Def = 0; for (unsigned i=0; i Def->Priority()) { Def = &Folders[i]; } } } } return Def; } bool HasMailStore(LXmlTag *MailStores, char *Name) { for (auto t : MailStores->Children) { char *StoreName = t->GetAttr(OPT_MailStoreName); if (StoreName && Name && !_stricmp(StoreName, Name)) return true; } return false; } LDataStoreI *ScribeWnd::CreateDataStore(const char *_Full, bool CreateIfMissing) { LString Full(_Full); auto Ext = LGetExtension(Full); if (Ext) { if (!_stricmp(Ext, "mail2")) { LgiMsg(this, LLoadString(IDS_MAIL2_DEPRECATED), AppName, MB_OK, Full); } else if (!_stricmp(Ext, "mail3")) { return OpenMail3(Full, this, CreateIfMissing); } else if (!_stricmp(Ext, "sqlite")) { LTrimDir(Full); return OpenMail3(Full, this, CreateIfMissing); } else { LgiTrace("%s:%i - Not a valid mail store extension: %s\n", _FL, Full); LAssert(!"Not a valid mail store extension."); } } else LgiTrace("%s:%i - No extension for CreateDataStore: %s\n", _FL, Full); return NULL; } class MailStoreUpgrade : public LProgressDlg, public LDataPropI { public: ScribeWnd *App = NULL; LDataStoreI *Ds = NULL; int Status = -1; LString Error; MailStoreUpgrade(ScribeWnd *app, LDataStoreI *ds) { App = app; Ds = ds; SetCanCancel(false); SetDescription("Upgrading mail store..."); Ds->Upgrade(this, this, [this](auto status) { Status = status; }); } ~MailStoreUpgrade() { } void OnPulse() override { if (Status >= 0) { EndModal(0); return; } return LProgressDlg::OnPulse(); } LDataPropI &operator =(LDataPropI &p) { LAssert(0); return *this; } Store3Status SetStr(int id, const char *str) override { switch (id) { case Store3UiError: Error = str; break; default: LAssert(!"Impl me."); return Store3Error; break; } return Store3Success; } }; bool ScribeWnd::ProcessFolder(LDataStoreI *&Store, int StoreIdx, char *StoreName) { if (Store->GetInt(FIELD_VERSION) == 0) { // version error LgiMsg(this, LLoadString(IDS_ERROR_FOLDERS_VERSION), AppName, MB_OK, 0, Store->GetInt(FIELD_VERSION)); return false; } if (Store->GetInt(FIELD_READONLY)) { LgiMsg(this, LLoadString(IDS_ERROR_READONLY_FOLDERS), AppName); } // get root item LDataFolderI *Root = Store->GetRoot(); if (!Root) return false; ScribeFolder *&Mailbox = Folders[StoreIdx].Root; Mailbox = new ScribeFolder; if (Mailbox) { Mailbox->App = this; Mailbox->SetObject(Root, false, _FL); Root->SetStr(FIELD_FOLDER_NAME, StoreName); Root->SetInt(FIELD_FOLDER_TYPE, MAGIC_NONE); } #ifdef TEST_OBJECT_SIZE // debug/repair code if (Root->StoreSize != Root->Sizeof()) { SizeErrors[0]++; Root->StoreSize = Root->Sizeof(); if (Root->Object) { Root->Object->StoreDirty = true; } } #endif // Insert the root object and then... Tree->Insert(Mailbox); // Recursively load the rest of the tree { LProfile p("Loadfolders"); Mailbox->LoadFolders(); } // This forces a re-pour to re-order the folders according to their // sort settings. Tree->UpdateAllItems(); if (ScribeState != ScribeExiting) { // Show the tree Mailbox->Expanded(Folders[StoreIdx].Expanded); // Checks the folders for a number of required objects // and creates them if required auto StoreType = Store->GetInt(FIELD_STORE_TYPE); if (StoreType == Store3Sqlite) Validate(&Folders[StoreIdx]); else if (StoreType < 0) LAssert(!"Make sure you impl the FIELD_STORE_TYPE field in the store."); // FIXME // AddFolderToMru(Full); } return true; } bool ScribeWnd::LoadMailStores() { bool Status = false; LXmlTag *MailStores = GetOptions()->LockTag(OPT_MailStores, _FL); if (!MailStores) return false; bool OptionsDirty = false; int StoreIdx = 0; for (auto MailStore: MailStores->Children) { if (!MailStore->IsTag(OPT_MailStore)) continue; // Read the folders.. auto Path = MailStore->GetAttr(OPT_MailStoreLocation); auto ContactUrl = MailStore->GetAttr(OPT_MailStoreContactUrl); auto CalUrl = MailStore->GetAttr(OPT_MailStoreCalendarUrl); if (!Path && !ContactUrl && !CalUrl) { LgiTrace("%s:%i - No mail store path (%i).\n", _FL, StoreIdx); continue; } char *StoreName = MailStore->GetAttr(OPT_MailStoreName); if (!StoreName) { char Tmp[256]; for (int i=1; true; i++) { sprintf_s(Tmp, sizeof(Tmp), "Folders%i", i); if (!HasMailStore(MailStores, Tmp)) break; } MailStore->SetAttr(OPT_MailStoreName, Tmp); StoreName = MailStore->GetAttr(OPT_MailStoreName); OptionsDirty = true; } Folders[StoreIdx].Name = StoreName; if (MailStore->GetAsInt(OPT_MailStoreDisable) > 0) { // LgiTrace("%s:%i - Mail store '%i' is disabled.\n", _FL, StoreIdx); continue; } if (Path) { // Mail3 folders on disk... char Full[MAX_PATH_LEN]; if (LIsRelativePath(Path)) { LMakePath(Full, sizeof(Full), GetOptions()->GetFile(), ".."); LMakePath(Full, sizeof(Full), Full, Path); } else { strcpy_s(Full, sizeof(Full), Path); } LVariant CreateFoldersIfMissing; GetOptions()->GetValue(OPT_CreateFoldersIfMissing, CreateFoldersIfMissing); // Sniff type... char *Ext = LGetExtension(Full); if (!Ext) continue; if (!Folders[StoreIdx].Store) Folders[StoreIdx].Store = CreateDataStore(Full, CreateFoldersIfMissing.CastInt32() != 0); if (!Folders[StoreIdx].Store) { LgiTrace("%s:%i - Failed to create data store for '%s'\n", _FL, Full); continue; } Folders[StoreIdx].Path = Full; } else if (ContactUrl || CalUrl) { // Remove Webdav folders... Folders[StoreIdx].Store = new WebdavStore(this, this, StoreName); } else break; LDataStoreI *&Store = Folders[StoreIdx].Store; auto ex = MailStore->GetAsInt(OPT_MailStoreExpanded); if (ex >= 0) Folders[StoreIdx].Expanded = ex != 0; // check if the mail store requires upgrading... Store3Status MsState = (Store3Status)Store->GetInt(FIELD_STATUS); if (MsState == Store3UpgradeRequired) { const char *Details = Store->GetStr(FIELD_STATUS); if (LgiMsg(this, LLoadString(IDS_MAILSTORE_UPGRADE_Q), AppName, MB_YESNO, Folders[StoreIdx].Path.Get(), ValidStr(Details)?Details:"n/a") == IDYES) { auto Prog = new MailStoreUpgrade(this, Store); Prog->DoModal(NULL); } else { continue; } } else if (MsState == Store3Error) { auto ErrMsg = Store->GetStr(FIELD_ERROR); auto a = new LAlert(this, AppName, ErrMsg ? ErrMsg : LLoadString(IDS_ERROR_FOLDERS_STATUS), LLoadString(IDS_EDIT_MAIL_STORES), LLoadString(IDS_OK)); a->DoModal([this](auto dlg, auto Btn) { if (Btn == 1) PostEvent(M_COMMAND, IDM_MANAGE_MAIL_STORES); delete dlg; }); continue; } // check password LString FolderPsw; if ((FolderPsw = Store->GetStr(FIELD_STORE_PASSWORD))) { bool Verified = false; if (ValidStr(d->MulPassword)) { Verified = d->MulPassword.Equals(FolderPsw, false); d->MulPassword.Empty(); } if (!Verified) { auto Dlg = new LInput(this, "", LLoadString(IDS_ASK_FOLDER_PASS), AppName, true); Dlg->DoModal([this, Dlg, FolderPsw, &Store, StoreIdx, StoreName](auto dlg, auto id) { if (id == IDOK) { GPassword User; User.Set(Dlg->GetStr()); if (Dlg->GetStr() == FolderPsw) ProcessFolder(Store, StoreIdx, StoreName); else DeleteObj(Store); } delete dlg; }); } } else ProcessFolder(Store, StoreIdx, StoreName); Status = true; StoreIdx++; } if (Status) { // Force load some folders... ScribeFolder *Folder = GetFolder(FOLDER_CALENDAR); if (Folder) Folder->LoadThings(); Folder = GetFolder(FOLDER_FILTERS); if (Folder) Folder->LoadThings(); for (auto ms: Folders) { if (!ms.Root) continue; for (auto c = ms.Root->GetChildFolder(); c; c = c->GetNextFolder()) { if (c->GetItemType() == MAGIC_CONTACT || c->GetItemType() == MAGIC_FILTER) c->LoadThings(); } } List c; GetContacts(c); // Set selected folder to Inbox by default // if the user hasn't selected a folder already if (ScribeState != ScribeExiting && Tree && !Tree->Selection()) { LVariant StartInFolder; GetOptions()->GetValue(OPT_StartInFolder, StartInFolder); ScribeFolder *Start = NULL; if (ValidStr(StartInFolder.Str())) { Start = GetFolder(StartInFolder.Str()); } if (!Start) { Start = GetFolder(FOLDER_INBOX); } if (Start && Tree) { Tree->Select(Start); } } } GetOptions()->DeleteValue(OPT_CreateFoldersIfMissing); if (OptionsDirty) SaveOptions(); GetOptions()->Unlock(); // Set system folders ScribeFolder *f = GetFolder(FOLDER_INBOX); if (f) f->SetSystemFolderType(Store3SystemInbox); f = GetFolder(FOLDER_OUTBOX); if (f) f->SetSystemFolderType(Store3SystemOutbox); f = GetFolder(FOLDER_SENT); if (f) f->SetSystemFolderType(Store3SystemSent); f = GetFolder(FOLDER_SPAM); if (f) f->SetSystemFolderType(Store3SystemSpam); return Status; } void ScribeWnd::LoadFolders(std::function Callback) { AppState PrevState = ScribeState; ScribeState = ScribeLoadingFolders; // Setup Mailstores tag { LXmlTag *MailStores = GetOptions()->LockTag(OPT_MailStores, _FL); if (!MailStores) { // Check if we can upgrade the old folder tag char n[32]; sprintf_s(n, sizeof(n), "%s-Folders", LGetOsName()); LVariant OldFolders; GetOptions()->GetValue(n, OldFolders); // Create mail store element.. GetOptions()->CreateTag(OPT_MailStores); if ((MailStores = GetOptions()->LockTag(OPT_MailStores, _FL))) { if (OldFolders.Str()) { LXmlTag *Store = MailStores->CreateTag(OPT_MailStore); if (Store) { char Opts[MAX_PATH_LEN]; LMakePath(Opts, sizeof(Opts), GetOptions()->GetFile(), ".."); auto Rel = LMakeRelativePath(Opts, OldFolders.Str()); Store->SetAttr(OPT_MailStoreLocation, Rel ? Rel.Get() : OldFolders.Str()); // No need to ask the user for a store name, it'll be // asked later in this method anyway... // Leave the old folder tag in the xml in case the user // downgrades to v1.xx } } } } GetOptions()->Unlock(); if (!MailStores) { if (Callback) Callback(false); return; } } // Set loading flags CmdSend.Enabled(false); CmdReceive.Enabled(false); CmdPreview.Enabled(false); bool Status = LoadMailStores(); if (Tree) { for (auto a: Accounts) { if (!a->Receive.Disabled() && a->Receive.IsPersistant()) a->Receive.Connect(0, false); } } using BoolFn = std::function; auto FinishLoad = new BoolFn ( [this, PrevState, Callback](bool Status) { if (ScribeState == ScribeExiting) { LCloseApp(); } else { d->FoldersLoaded = true; PostEvent(M_SCRIBE_LOADED); } if (ScribeState == ScribeExiting) LCloseApp(); ScribeState = PrevState; if (Callback) Callback(Status); } ); if (Folders.Length() == 0) { auto Dlg = new ScribeFolderDlg(this); Dlg->DoModal([this, Dlg, FinishLoad, Callback, &Status](auto dlg, auto id) { if (id == IDOK) { bool CreateMailStore = false; if (Dlg->Create) { // create folders if (LFileExists(Dlg->FolderFile)) { if (LgiMsg(this, LLoadString(IDS_ERROR_FOLDERS_ALREADY_EXIST), AppName, MB_YESNO) == IDYES) CreateMailStore = true; else LgiMsg(this, LLoadString(IDS_ERROR_WONT_OVERWRITE_FOLDERS), AppName); } else if ((Status = CreateFolders(Dlg->FolderFile))) CreateMailStore = true; } else CreateMailStore = true; if (CreateMailStore) { LXmlTag *MailStores = GetOptions()->LockTag(OPT_MailStores, _FL); if (MailStores) { LXmlTag *Store = MailStores->CreateTag(OPT_MailStore); if (Store) { char p[MAX_PATH_LEN]; LMakePath(p, sizeof(p), GetOptions()->GetFile(), ".."); auto RelPath = LMakeRelativePath(p, Dlg->FolderFile); Store->SetAttr(OPT_MailStoreLocation, RelPath ? RelPath.Get() : Dlg->FolderFile.Get()); } GetOptions()->Unlock(); LoadMailStores(); } } } if (id) (*FinishLoad)(Status); else if (Callback) Callback(false); delete FinishLoad; delete dlg; }); } else { (*FinishLoad)(Status); delete FinishLoad; } } bool ScribeWnd::UnLoadFolders() { if (FolderTasks.Length() > 0 || ScribeState == ScribeLoadingFolders) { // Um, we can't unload folders right now // something is already happening... return false; } AppState PrevState = ScribeState; ScribeState = ScribeUnloadingFolders; OnSelect(); if (MailList) { ScribeFolder *Container = MailList->GetContainer(); if (Container) { // save folder settings Container->SerializeFieldWidths(); } MailList->SetContainer(NULL); MailList->RemoveAll(); } int Error = 0; while (Thing::DirtyThings.Length() > 0) { if (!SaveDirtyObjects()) { Error++; LgiTrace("%s:%i - SaveDirtyObjects failed.\n", _FL); if (Error > 100) { // I think we're stuck... return false; } } } // Unload IMAP folders... for (auto a: Accounts) { if (!a->Receive.Disabled() && a->Receive.IsPersistant()) { a->Receive.Disconnect(); } } if (GetOptions()) { // Unload local folders... LXmlTag *MailStores = GetOptions()->LockTag(OPT_MailStores, _FL); for (size_t i=0; iExpanded(); for (auto ms: MailStores->Children) { char *StoreName = ms->GetAttr(OPT_MailStoreName); if (Folders[i].Name.Equals(StoreName)) { ms->SetAttr(OPT_MailStoreExpanded, Expanded); break; } } } DeleteObj(Folders[i].Root); DeleteObj(Folders[i].Store); } if (MailStores) { GetOptions()->Unlock(); MailStores = NULL; } } Folders.Length(0); d->FoldersLoaded = false; if (ScribeState == ScribeExiting) LCloseApp(); ScribeState = PrevState; return true; } void ScribeWnd::BuildDynMenus() { if (MailMenu) { LString SendMail = LLoadString(IDS_SEND_MAIL); LString ReceiveMail = LLoadString(IDS_RECEIVE_MAIL); LString PreviewMail = LLoadString(IDS_PREVIEW_ON_SERVER); auto ReceiveAll = LLoadString(IDS_RECEIVE_ALL_ACCOUNTS); if (!CmdReceive.MenuItem && ReceiveAll) CmdReceive.MenuItem = MailMenu->AppendItem(ReceiveAll, IDM_RECEIVE_ALL, true); if (!SendMenu && SendMail) { auto s = SendMail.SplitDelimit("\t"); SendMenu = MailMenu->AppendSub(s[0]); } if (!ReceiveMenu && ReceiveMail) { auto s = ReceiveMail.SplitDelimit("\t"); ReceiveMenu = MailMenu->AppendSub(s[0]); } if (!PreviewMenu && PreviewMail) { auto s = PreviewMail.SplitDelimit("\t"); PreviewMenu = MailMenu->AppendSub(s[0]); } } if (!NewTemplateMenu) { auto i = Menu->FindItem(IDM_NO_TEMPLATES); if (i) { NewTemplateMenu = i->GetParent(); } } if (NewTemplateMenu) { NewTemplateMenu->Empty(); int d = 0; ScribeFolder *Templates = GetFolder(FOLDER_TEMPLATES, NULL, true); if (Templates) { Templates->LoadThings(); for (auto i: Templates->Items) { Mail *m = i->IsMail(); if (m) { NewTemplateMenu->AppendItem(m->GetSubject() ? m->GetSubject() : (char*)"(no subject)", IDM_NEW_FROM_TEMPLATE + d, true); d++; } } if (d == 0) { NewTemplateMenu->AppendItem(LLoadString(IDS_NO_ITEMS_IN_FOLDER), -1, false); } } else { NewTemplateMenu->AppendItem(LLoadString(IDS_NO_TEMPLATES), -1, false); } } } int ScribeWnd::GetToolbarHeight() { return (Commands) ? MAX(Commands->Y()-1, 20) : 20; } LToolBar *ScribeWnd::LoadToolbar(LViewI *Parent, const char *File, LAutoPtr &Img) { if (!Img) Img.Reset(LLoadImageList(File)); if (!Img) { LAssert(!"Missing image resource."); return NULL; } LToolBar *Tools = NULL; if (Img) { Tools = new LToolBar; if (Tools) Tools->SetImageList(Img, Img->TileX(), Img->TileY(), false); } else { Tools = LgiLoadToolbar(Parent, File); } if (Tools) { LVariant SizeAdj; LFont *f = Tools->GetFont(); if (f) { if (GetOptions()->GetValue(OPT_UiFontSize, SizeAdj)) { SizeAdj.Cast(GV_INT32); SizeAdj.Value.Int -= 2; f->PointSize(f->PointSize()+SizeAdj.Value.Int); } } Tools->GetCss(true)->BorderSpacing(LCss::Len(LCss::LenPx, SCRIBE_TOOLBAR_BORDER_SPACING_PX)); Tools->TextLabels(ShowToolbarText()); } return Tools; } void ScribeWnd::SetListPane(LView *v) { ThingList *ThingLst = dynamic_cast(v); DynamicHtml *Html = dynamic_cast(v); if (!ThingLst) { DeleteObj(SearchView); if (MailList) MailList->RemoveAll(); } v->Sunken(SUNKEN_CTRL); if ((MailList = ThingLst)) { DeleteObj(TitlePage); if (GetCtrlValue(IDM_ITEM_FILTER)) { OnCommand(IDM_ITEM_FILTER, 0, NULL); } } else { TitlePage = Html; } SetLayout(); } bool ScribeWnd::SetItemPreview(LView *v) { v->Sunken(SUNKEN_CTRL); if (d->SubSplit->IsAttached()) { if (d->LastLayout == 2) { Splitter->SetViewAt(1, v); } else { d->SubSplit->SetViewAt(1, v); } return true; } return false; } ScribeWnd::LayoutMode ScribeWnd::GetEffectiveLayoutMode() { LVariant Mode; GetOptions()->GetValue(OPT_LayoutMode, Mode); ScribeFolder *Cur = GetCurrentFolder(); if (Cur && Cur->IsRoot()) { Mode = FoldersAndList; } if (Mode.CastInt32() == 0) { Mode = FoldersListAndPreview; } return (LayoutMode) Mode.CastInt32(); } void ScribeWnd::SetLayout(LayoutMode Mode) { // int TreeWidth = Tree ? Tree->X() : 200; // int PreviewHt = PreviewPanel ? PreviewPanel->Y() : 200; if (Mode > 0) { LVariant v; GetOptions()->SetValue(OPT_LayoutMode, v = (int)Mode); } Mode = GetEffectiveLayoutMode(); if (!Splitter) return; bool JustPreviewPane = (Mode == FoldersAndList && d->LastLayout == FoldersListAndPreview) || (Mode == FoldersListAndPreview && d->LastLayout == FoldersAndList); LView *Content = NULL; if (TitlePage) Content = TitlePage; else if (MailList) Content = MailList; if (JustPreviewPane) { // Optimized path for hide/show the preview pane that doesn't destroy the tree // control and cause it to lose focus... otherwise we can't set it's focus due // to some weird windows interaction. switch (Mode) { default: case FoldersListAndPreview: { if (Content) Content->Sunken(SUNKEN_CTRL); if (PreviewPanel) PreviewPanel->Sunken(SUNKEN_CTRL); Splitter->SetViewAt(1, d->SubSplit); d->SubSplit->SetVertical(true); int Idx = 0; if (SearchView) d->SubSplit->SetViewAt(Idx++, SearchView); d->SubSplit->SetViewAt(Idx++, Content); d->SubSplit->SetViewAt(Idx++, PreviewPanel); break; } case FoldersAndList: { if (Content) Content->Sunken(SUNKEN_CTRL); if (SearchView) { #if LGI_VIEW_HANDLE if (!d->SubSplit->Handle()) Splitter->SetViewAt(1, d->SubSplit); #endif d->SubSplit->SetVertical(true); Splitter->SetViewAt(1, d->SubSplit); int Idx = 0; if (SearchView) d->SubSplit->SetViewAt(Idx++, SearchView); d->SubSplit->SetViewAt(Idx++, Content); } else { d->SubSplit->Detach(); Splitter->SetViewAt(1, Content); } break; } } } else { if (Tree) Tree->Sunken(SUNKEN_CTRL); if (Content) Content->Sunken(SUNKEN_CTRL); if (PreviewPanel) PreviewPanel->Sunken(SUNKEN_CTRL); switch (Mode) { default: case FoldersListAndPreview: { Splitter->SetVertical(false); d->SubSplit->SetVertical(true); Splitter->SetViewAt(0, Tree); Splitter->SetViewAt(1, d->SubSplit); int Idx = 0; if (SearchView) d->SubSplit->SetViewAt(Idx++, SearchView); d->SubSplit->SetViewAt(Idx++, Content); d->SubSplit->SetViewAt(Idx++, PreviewPanel); DeleteObj(d->SearchSplit); break; } case PreviewOnBottom: { Splitter->SetVertical(true); d->SubSplit->SetVertical(false); Splitter->SetViewAt(0, d->SubSplit); Splitter->SetViewAt(1, PreviewPanel); d->SubSplit->SetViewAt(0, Tree); if (SearchView) { if (!d->SearchSplit) d->SearchSplit = new LBox; d->SubSplit->SetViewAt(1, d->SearchSplit); d->SearchSplit->SetVertical(true); d->SearchSplit->SetViewAt(0, SearchView); d->SearchSplit->SetViewAt(1, Content); } else { d->SubSplit->SetViewAt(1, Content); DeleteObj(d->SearchSplit); } break; } case FoldersAndList: { Splitter->SetVertical(false); Splitter->SetViewAt(0, Tree); if (SearchView) { d->SubSplit->SetVertical(true); Splitter->SetViewAt(1, d->SubSplit); d->SubSplit->SetViewAt(0, SearchView); d->SubSplit->SetViewAt(1, Content); } else { d->SubSplit->Detach(); Splitter->SetViewAt(1, Content); } DeleteObj(d->SearchSplit); break; } case ThreeColumn: { Splitter->SetVertical(false); Splitter->SetViewAt(0, Tree); if (SearchView) { d->SubSplit->SetVertical(true); Splitter->SetViewAt(1, d->SubSplit); d->SubSplit->SetViewAt(0, SearchView); d->SubSplit->SetViewAt(1, Content); } else { d->SubSplit->Detach(); Splitter->SetViewAt(1, Content); } Splitter->SetViewAt(2, PreviewPanel); DeleteObj(d->SearchSplit); break; } } } if (!SearchView) { LVariant Pos = 200; GetOptions()->GetValue(OPT_SplitterPos, Pos); if (Pos.CastInt32() < 10) Pos = 200; Splitter->Value(Pos.CastInt32()); LRect r = Splitter->GetPos(); r.x2++; Splitter->SetPos(r); if (d->SubSplit->IsAttached()) { Pos = 200; GetOptions()->GetValue(OPT_SubSplitPos, Pos); if (Pos.CastInt32() < 10) Pos = 200; d->SubSplit->Value(Pos.CastInt32()); } } PourAll(); #ifdef LINUX LYield(); #endif d->LastLayout = Mode; } void ScribeWnd::SetupUi() { // Show the window if (!SerializeState(GetOptions(), OPT_ScribeWndPos, true)) { LRect r(0, 0, 1023, 767); SetPos(r); MoveToCenter(); } Visible(true); // Main toolbar Commands = LoadToolbar(this, GetResourceFile(ResToolbarFile), ToolbarImgs); if (Commands) { Commands->Attach(this); #ifdef MAC Commands->Raised(false); #else Commands->Raised(RAISED_LOOK); #endif Commands->AppendButton(RemoveAmp(LLoadString(IDS_NEW_EMAIL)), IDM_NEW_EMAIL, TBT_PUSH, true, IMG_NEW_MAIL); Commands->AppendButton(RemoveAmp(LLoadString(IDS_NEW_CONTACT)), IDM_NEW_CONTACT, TBT_PUSH, true, IMG_NEW_CONTACT); Commands->AppendSeparator(); CmdSend.ToolButton = Commands->AppendButton(RemoveAmp(LLoadString(IDS_SEND)), IDM_SEND_MAIL, TBT_PUSH, true, IMG_SEND); Commands->AppendButton("+", IDM_RECEIVE_AND_SEND, TBT_PUSH, true, -2); CmdReceive.ToolButton = Commands->AppendButton(RemoveAmp(LLoadString(IDS_RECEIVE)), IDM_RECEIVE_MAIL, TBT_PUSH, true, IMG_RECEIVE); CmdPreview.ToolButton = Commands->AppendButton(RemoveAmp(LLoadString(IDS_PREVIEW)), IDM_PREVIEW_POP3, TBT_PUSH, true, IMG_PREVIEW); Commands->AppendSeparator(); Commands->AppendButton(RemoveAmp(LLoadString(IDS_DELETE)), IDM_DELETE, TBT_PUSH, true, IMG_TRASH); Commands->AppendButton(RemoveAmp(LLoadString(IDS_SPAM)), IDM_DELETE_AS_SPAM, TBT_PUSH, true, IMG_DELETE_SPAM); Commands->AppendSeparator(); Commands->AppendButton(RemoveAmp(LLoadString(IDS_REPLY)), IDM_REPLY, TBT_PUSH, true, IMG_REPLY); Commands->AppendButton(RemoveAmp(LLoadString(IDS_REPLYALL)), IDM_REPLY_ALL, TBT_PUSH, true, IMG_REPLY_ALL); Commands->AppendButton(RemoveAmp(LLoadString(IDS_FORWARD)), IDM_FORWARD, TBT_PUSH, true, IMG_FORWARD); Commands->AppendButton(RemoveAmp(LLoadString(IDS_BOUNCE)), IDM_BOUNCE, TBT_PUSH, true, IMG_BOUNCE); Commands->AppendSeparator(); Commands->AppendButton(RemoveAmp(LLoadString(IDS_PRINT)), IDM_PRINT, TBT_PUSH, true, IMG_PRINT); Commands->AppendButton(RemoveAmp(LLoadString(IDS_CALENDAR)), IDM_CALENDAR, TBT_PUSH, true, IMG_CALENDAR); Commands->AppendButton(RemoveAmp(LLoadString(IDS_ITEM_FILTER)), IDM_ITEM_FILTER, TBT_TOGGLE, true, IMG_SEARCH); Commands->AppendButton(RemoveAmp(LLoadString(IDS_THREAD)), IDM_THREAD, TBT_TOGGLE, true, IMG_THREADS); d->ShowConsoleBtn = Commands->AppendButton(RemoveAmp(LLoadString(IDS_SHOW_CONSOLE)), IDM_SHOW_CONSOLE, TBT_PUSH, true, IMG_CONSOLE_NOMSG); Commands->AppendSeparator(); Commands->AppendButton(RemoveAmp(LLoadString(IDS_HELP)), IDM_HELP, TBT_PUSH, true, IMG_HELP); Commands->Customizable(GetOptions(), OPT_ScribeWndToolbar); if (d->ScriptToolbar.Reset(new LScriptUi(Commands))) d->ScriptToolbar->SetupCallbacks(this, 0, 0, LApplicationToolbar); PourAll(); } CmdSend.Enabled(false); CmdReceive.Enabled(false); // Preview and status windows PreviewPanel = new LPreviewPanel(this); StatusPanel = new AccountStatusPanel(this, ImageList); if (PreviewPanel && StatusPanel) { #ifdef MAC StatusPanel->Raised(false); #else StatusPanel->Raised(RAISED_LOOK); #endif StatusPanel->Attach(this); PourAll(); } // Splitter window, for folders and item list Tree = new MailTree(this); if (ImageList) { Tree->AskImage(true); Tree->SetImageList(ImageList, false); } d->SubSplit = new LBox; Splitter = new LBox; if (Splitter) { Splitter->Attach(this); #ifdef MAC Splitter->Raised(false); #else Splitter->Raised(RAISED_LOOK); #endif SetLayout(); } #if WINNATIVE TrayIcon.Load(MAKEINTRESOURCE(IDI_SMALL)); TrayIcon.Load(MAKEINTRESOURCE(IDI_ERR)); TrayIcon.Load(MAKEINTRESOURCE(IDI_MAIL)); TrayIcon.Load(MAKEINTRESOURCE(IDI_BLANK)); #else TrayIcon.Load(_T("tray_small.png")); TrayIcon.Load(_T("tray_error.png")); TrayIcon.Load(_T("tray_mail.png")); #endif LStringPipe s(256); LVariant UserName; s.Print("%s", AppName); if (GetOptions()->GetValue(OPT_UserName, UserName)) { s.Print(" [%s]", UserName.Str()); } auto AppTitle = s.NewGStr(); TrayIcon.Name(AppTitle); Name(AppTitle); TrayIcon.Value(TRAY_ICON_NORMAL); TrayIcon.Visible(true); auto Item = Menu->FindItem(IDM_SCRIPTING_CONSOLE); if (Item) { LVariant v; if (GetOptions()->GetValue(OPT_ShowScriptConsole, v)) { Item->Checked(v.CastInt32() != 0); LScribeScript::Inst->ShowScriptingWindow(Item->Checked()); } } if (Tree) { Tree->Focus(true); } } Thing *ScribeWnd::CreateItem(int Type, ScribeFolder *Folder, bool Ui) { auto FolderStore = Folder && Folder->GetObject() ? Folder->GetObject()->GetStore() : NULL; auto DefaultStore = GetDefaultMailStore(); auto Store = FolderStore ? FolderStore : (DefaultStore ? DefaultStore->Store : NULL); if (!Store) { LAssert(!"no store"); LgiTrace("%s:%i - No store for creating calendar object.\n", _FL); return NULL; } auto Obj = Store->Create(Type); if (!Obj) { LAssert(!"create failed"); LgiTrace("%s:%i - store failed to create object.\n", _FL); return NULL; } #define HANDLE_CREATE_ITEM(Magic, Type) \ case Magic: \ { \ auto o = new Type(this, Obj); \ if (!o) \ { \ LgiTrace("%s:%i - Alloc failed.\n", _FL); \ break; \ } \ if (Folder) o->SetParentFolder(Folder); \ if (Ui) o->DoUI(); \ return o; \ } switch ((uint32_t)Type) { case MAGIC_MAIL: { // create a new mail message Mail *m = new Mail(this, Obj); if (!m) { LgiTrace("%s:%i - Alloc failed.\n", _FL); break; } if (!m->GetObject()) { m->DecRef(); return 0; } m->OnCreate(); if (Folder) m->SetParentFolder(Folder); if (Ui) m->DoUI(); return m; } HANDLE_CREATE_ITEM(MAGIC_CONTACT, Contact) HANDLE_CREATE_ITEM(MAGIC_CALENDAR, Calendar) HANDLE_CREATE_ITEM(MAGIC_FILTER, Filter) HANDLE_CREATE_ITEM(MAGIC_GROUP, ContactGroup) default: LAssert(!"Unhandled object type."); break; } return NULL; } void ScribeWnd::OnPaint(LSurface *pDC) { #if 0 pDC->Colour(LColour::Red); pDC->Rectangle(); #else LCssTools Tools(this); auto c = GetClient(); // c.Offset(0, -26); Tools.PaintContent(pDC, c); #endif } int CompareContacts(Contact **a, Contact **b) { if (a && b) { auto A = (*a)->GetFirst(); auto B = (*b)->GetFirst(); if (A && B) return _stricmp(A, B); } return 0; } bool ScribeWnd::OpenAMail(ScribeFolder *Folder) { if (Folder && Tree) { Folder->LoadThings(); for (auto i: Folder->Items) { Mail *m = i->IsMail(); if (m && !(m->GetFlags() & MAIL_READ)) { Tree->Select(Folder); m->DoUI(); return true; } } for (auto *t=Folder->GetChild(); t; t=t->GetNext()) { ScribeFolder *f = dynamic_cast(t); if (OpenAMail(f)) { return true; } } } return false; } void AddContactToMenu(LSubMenu *Menu, Contact *c, ssize_t Index) { if (!c || Index < 0) return; auto Email = c->GetEmail(); if (!Email) return; // has an email, list it auto First = c->GetFirst(); auto Last = c->GetLast(); if (First || Last) { char Buf[256]; sprintf_s(Buf, sizeof(Buf), "%s %s", (First)?First:"", (Last)?Last:""); auto Item = Menu->AppendItem(Buf, TRAY_CONTACT_BASE + (int)Index, true); if (Item) Item->Icon(ICON_CONTACT); } } void ScribeWnd::AddContactsToMenu(LSubMenu *Menu) { if (!Menu) return; d->TrayMenuContacts.Sort(CompareContacts); if (((ssize_t)d->TrayMenuContacts.Length() << 4) > GdcD->Y() - 200) { // Group contacts by starting letter LArray Alpha[26]; LArray Other; for (auto c: d->TrayMenuContacts) { auto First = c->GetFirst(); auto Last = c->GetLast(); auto Email = c->GetEmail(); if (Email) { // has an email, list it if (First || Last) { auto Name = First?First:Last; if ( (*Name >= 'a' && *Name <= 'z') || (*Name >= 'A' && *Name <= 'Z') ) { int Ind = tolower(*Name) - 'a'; if (Ind >= 0 && Ind < CountOf(Alpha)) Alpha[Ind].Add(c); else Other.Add(c); } else Other.Add(c); } } } for (int i=0; i 0) { char Group[64]; sprintf_s(Group, sizeof(Group), "%c...", 'a' + i); auto Sub = Menu->AppendSub(Group); if (Sub) { for (auto c: Alpha[i]) AddContactToMenu(Sub, c, d->TrayMenuContacts.IndexOf(c)); } } } if (Other.Length()) { auto Sub = Menu->AppendSub("Other..."); if (Sub) { for (auto c: Other) AddContactToMenu(Sub, c, d->TrayMenuContacts.IndexOf(c)); } } } else { // Display all... for (size_t i=0; iTrayMenuContacts.Length(); i++) { AddContactToMenu(Menu, d->TrayMenuContacts[i], i); } } } void ScribeWnd::OnUrl(const char *Url) { LUri u(Url); if (u.sProtocol && !_stricmp(u.sProtocol, "mailto")) { CreateMail(0, Url, 0); } } void ScribeWnd::OnReceiveFiles(LArray &Files) { int UnknownFormats = 0; int64 Period = LCurrentTime() - LastDrop; if (Period > 500) // Lock out drops within 500ms of an LGI drop { LString sSend, sPages; bool HasSend = LAppInst->GetOption("send", sSend); bool HasPrint = LAppInst->GetOption("p", sPages); LArray NewMail; for (unsigned i=0; i str(new LFile); if (str->Open(f, O_READ)) { if (t->Import(t->AutoCast(str), MimeType)) { if (HasSend) { Mail *m = t->IsMail(); if (m) { NewMail.Add(m); m->SetFlags(m->GetFlags() | MAIL_CREATED | MAIL_READ); } } else { t->DoUI(); } } } } } else UnknownFormats++; } bool SendNow = sSend ? atoi(sSend) != 0 : false; for (unsigned i=0; iSetFlags(m->GetFlags() | MAIL_READY_TO_SEND); m->Save(); } else if (HasPrint) { ThingPrint(NULL, m, GetPrinter(), 0, sPages ? atoi(sPages) : 0); } else { m->DoUI(); } } if (SendNow) Send(); } } void ScribeWnd::OnTrayClick(LMouse &m) { if (m.Down()) { #ifndef MAC // No support for different mouse button info so the default // action is to show the sub-menu of contacts and actions. if (m.IsContextMenu()) #endif { LWindow::OnTrayClick(m); } #ifndef MAC else if (m.Left()) { if (m.Double()) { if (Mail::NewMailLst.Length() > 0) { ScribeFolder *InBox = GetFolder(FOLDER_INBOX); OpenAMail(InBox); } } else { if (GetZoom() == LZoomMin) { SetZoom(LZoomNormal); } else { if (Obscured()) SetZoom(LZoomNormal); // Bounce in front, first. else SetZoom(LZoomMin); } Visible(true); MoveOnScreen(); Raise(); if (MailList) { MailList->Focus(true); } } } else if (m.Middle()) { Mail::NewMailLst.Empty(); } #endif } } void ScribeWnd::OnTrayMenu(LSubMenu &m) { m.SetImageList(ImageList, false); d->TrayMenuContacts.Length(0); #ifdef MAC m.AppendItem(LLoadString(IDS_OPEN), IDM_OPEN); m.AppendSeparator(); #endif LHashTbl,Contact*> Added; LArray Srcs = GetThingSources(MAGIC_CONTACT); for (auto c: Srcs) { c->LoadThings(); for (auto i: c->Items) { Contact *c = i->IsContact(); if (!c) continue; bool IsAdded = false; auto Emails = c->GetEmails(); for (auto e: Emails) { if (Added.Find(e)) IsAdded = true; } if (Emails.Length() && !IsAdded) { for (auto e: Emails) Added.Add(e, c); d->TrayMenuContacts.Add(c); } } } AddContactsToMenu(&m); m.AppendSeparator(); if (Mail::NewMailLst.Length() > 0) { int i=0; for (auto ml: Mail::NewMailLst) { LStringPipe p; // This code figures out how many UTF characters to print. We // can't split a UTF character because downstream conversions // will fail. const char *Subj = ml->GetSubject(); if (!Subj) Subj = "(No Subject)"; LUtf8Ptr u((uint8_t*)Subj); while ((int32)u && u.GetPtr() - (uchar*)Subj < 64) u++; ssize_t Bytes = u.GetPtr() - (uchar*)Subj; p.Print("%.*s, %s <%s>", Bytes, Subj?Subj:(char*)"(No Subject)", ml->GetFromStr(FIELD_NAME), ml->GetFromStr(FIELD_EMAIL)); LAutoString a(p.NewStr()); LAssert(LIsUtf8(a)); auto Item = m.AppendItem(a, TRAY_MAIL_BASE+i++, true); if (Item) { Item->Icon(ICON_UNREAD_MAIL); } } m.AppendSeparator(); } if (GetZoom() == LZoomMin) { m.AppendItem(LLoadString(IDS_OPEN), IDM_OPEN, true); } auto NewMail = m.AppendItem(LLoadString(IDS_NEW_EMAIL), IDM_NEW_EMAIL); if (NewMail) NewMail->Icon(ICON_UNSENT_MAIL); m.AppendItem(LLoadString(IDS_EXIT), IDM_EXIT); } void ScribeWnd::OnTrayMenuResult(int MenuId) { switch (MenuId) { case IDM_OPEN: { if (GetZoom() == LZoomMin) { SetZoom(LZoomNormal); } Visible(true); Raise(); if (MailList) { MailList->Focus(true); } break; } case IDM_NEW_EMAIL: { CreateMail(); break; } case IDM_EXIT: { d->IngoreOnClose = true; PostEvent(M_CLOSE); break; } default: { auto i = MenuId - TRAY_CONTACT_BASE; Contact *c = d->TrayMenuContacts.IdxCheck(i) ? d->TrayMenuContacts[i] : NULL; if (c) { CreateMail(c); } Mail *m = Mail::NewMailLst[MenuId - TRAY_MAIL_BASE]; if (m) { Mail::NewMailLst.Delete(m); m->DoUI(); } break; } } } void ScribeWnd::OnZoom(LWindowZoom Action) { if (Action == LZoomMin) { LVariant i; if (GetOptions()->GetValue(OPT_MinimizeToTray, i) && i.CastInt32()) { Visible(false); } } } struct UserInput { std::function Callback; LView *Parent; LString Msg; bool Password; UserInput() { Password = false; } }; void ScribeWnd::GetUserInput(LView *Parent, LString Msg, bool Password, std::function Callback) { if (InThread()) { auto Inp = new LInput(Parent ? Parent : this, "", Msg, AppName, Password); Inp->DoModal([this, Inp, Callback](auto dlg, auto id) { if (Callback) Callback(id ? Inp->GetStr() : NULL); delete dlg; }); } auto i = new UserInput; i->Parent = Parent; i->Msg = Msg; i->Password = Password; i->Callback = Callback; if (!PostEvent(M_GET_USER_INPUT, (LMessage::Param)i)) { LAssert(!"PostEvent failed."); if (Callback) Callback(NULL); } } LMessage::Result ScribeWnd::OnEvent(LMessage *Msg) { TrayIcon.OnEvent(Msg); BayesianFilter::OnEvent(Msg); switch (Msg->Msg()) { case M_UNIT_TEST: { LAutoPtr j((LJson*)Msg->A()); if (!j) break; auto cmd = j->Get("cmd"); if (!cmd) break; if (cmd.Equals("somecmd")) { } break; } case M_CALENDAR_SOURCE_EVENT: { CalendarSource *cs = (CalendarSource*)Msg->A(); LAutoPtr m((LMessage*)Msg->B()); if (cs && m) cs->OnEvent(m); break; } case M_GET_USER_INPUT: { LAutoPtr i((UserInput*)Msg->A()); LAssert(i); LAssert(InThread()); // Least we get stuck in an infinite loop GetUserInput(i->Parent, i->Msg, i->Password, i->Callback); break; } case M_SET_HTML: { LAutoPtr Html((LString*)Msg->A()); if (PreviewPanel && Html) { LVariant Ret; LArray Arg; Arg.Add(new LVariant(Html->Get())); PreviewPanel->CallMethod(DomToStr(SdSetHtml), &Ret, Arg); Arg.DeleteObjects(); } break; } case M_NEW_CONSOLE_MSG: { if (d->ShowConsoleBtn) d->ShowConsoleBtn->Image(IDM_CONSOLE_MSGS); break; } case M_NEEDS_CAP: { LAutoString c((char*)Msg->A()); LAutoString param((char*)Msg->B()); NeedsCapability(c, param); return 0; break; } case M_STORAGE_EVENT: { LDataStoreI *Store = LDataStoreI::Map.Find((int)Msg->A()); if (Store) Store->OnEvent((void*)Msg->B()); break; } case M_SCRIBE_SET_MSG_FLAG: { LAutoString p((char*)Msg->A()); if (p) { char *d = strrchr(p, '/'); if (d) { *d++ = 0; ScribeFolder *f = GetFolder(p); if (f) { LUri u; LString a = u.DecodeStr(d); f->GetMessageById(a, [&](auto r) { if (r) { int ExistingFlags = r->GetFlags(); int NewFlag = (int)Msg->B(); r->SetFlags(ExistingFlags | NewFlag); } }); } } } break; } case M_SCRIBE_DEL_THING: { Thing *t = (Thing*)Msg->A(); DeleteObj(t); break; } case M_SCRIBE_LOADED: { if (d->FoldersLoaded) { // Ok let the user in CmdSend.Enabled(true); CmdReceive.Enabled(true); CmdPreview.Enabled(true); } break; } case M_SCRIBE_THREAD_DONE: { // Finialize connection AccountThread *Thread = (AccountThread*) Msg->A(); Accountlet *Acc = (Accountlet*) Msg->B(); if (Thread && Acc) { OnAfterConnect(Acc->GetAccount(), Acc->IsReceive()); d->NewMailTimeout = 2; Acc->OnThreadDone(); } else { LAssert(0); } break; } case M_SCRIBE_MSG: { char *m = (char*)Msg->A(); if (m) { if (Msg->B()) { if (LgiMsg(this, m, AppName, MB_YESNO) == IDYES) { PostEvent(M_COMMAND, IDM_OPTIONS, 0); } } else { LgiMsg(this, "%s", AppName, MB_OK, m); } DeleteArray(m); } break; } /* case M_SCRIBE_LOG_MSG: { List *Log = (List*)Msg->A(); LAutoPtr Entry((LogEntry*)Msg->B()); if (ScribeState != ScribeExiting && Log && Entry) { Log->Insert(Entry.Release()); // Trim long list... while (Log->Length() > 1024) { LogEntry *e = Log->First(); Log->Delete(e); DeleteObj(e); } } break; } */ case M_SCRIBE_NEW_MAIL: { if (Lock(_FL)) { if (NewMailDlg) { NewMailDlg->AddThings(&Mail::NewMailLst); } Unlock(); } break; } case M_SCRIBE_OPEN_THING: { Thing *t = (Thing*) Msg->A(); if (t) { t->DoUI(); } break; } case M_SCRIBE_ITEM_SELECT: { if (!MailList || !IsAttached()) break; List Things; MailList->GetSelection(Things); OnSelect(&Things); break; } case M_URL: { LAutoPtr Url((LString*)Msg->A()); if (Url) { LUri u(*Url); if (u.sProtocol && !_stricmp(u.sProtocol, "mailto")) { Mailto mt(this, *Url); if (mt.To.Length() > 0) { Thing *t = CreateItem(MAGIC_MAIL, NULL, false); if (t) { Mail *m = t->IsMail(); if (m) { mt.Apply(m); m->DoUI(); } else DeleteObj(t); } } } } break; } } return LWindow::OnEvent(Msg); } bool ScribeWnd::IsMyEmail(const char *Email) { if (Email) { LVariant e; for (auto a : *GetAccounts()) { LVariant e = a->Identity.Email(); if (e.Str() && _stricmp(Email, e.Str()) == 0) { return true; } } } return false; } int ScribeWnd::GetMaxPages() { return d->PrintMaxPages; } void ScribeWnd::ThingPrint(std::function Callback, ThingType *m, LPrinter *Printer, LView *Parent, int MaxPages) { d->PrintMaxPages = MaxPages; if (!Printer) Printer = GetPrinter(); if (!Printer) { if (Callback) Callback(false); return; } Thing *t = dynamic_cast(m); if (!t) { if (Callback) Callback(false); return; } ScribePrintContext Events(this, t); Printer->Print( &Events, [&](auto pages) { if (pages == Events.OnBeginPrintError) { LgiMsg(Parent, "Printing failed: %s", AppName, MB_OK, Printer->GetErrorMsg().Get()); if (Callback) Callback(false); } else if (Callback) { Callback(true); } }, AppName, -1, Parent ? Parent : this); } bool ScribeWnd::MailReplyTo(Mail *m, bool All) { bool Status = false; if (m) { LDataStoreI *Store = m->GetObject() ? m->GetObject()->GetStore() : NULL; LDataI *NewMailObj = Store && Store->GetInt(FIELD_STORE_TYPE) == Store3Sqlite ? Store->Create(MAGIC_MAIL) : NULL; Mail *NewMail = new Mail(this, NewMailObj); if (NewMail) { if (NewMail->GetObject()) { NewMail->OnReply(m, All, true); LView *w = NewMail->DoUI(); if (w) { LViewI *t = w->FindControl(IDC_TEXT); if (t) { t->Focus(true); } } Status = true; } else DeleteObj(NewMail); } } return Status; } bool ScribeWnd::MailForward(Mail *m) { bool Status = false; if (m) { Mail *NewMail = new Mail(this); if (NewMail) { if (NewMail->OnForward(m, true)) { NewMail->DoUI(); Status = true; } else { NewMail->DecRef(); } } } return Status; } bool ScribeWnd::MailBounce(Mail *m) { bool Status = false; if (m) { Mail *NewMail = new Mail(this); if (NewMail) { if (NewMail->OnBounce(m, true)) { NewMail->DoUI(); Status = true; } else { DeleteObj(NewMail); } } } return Status; } Mail *ScribeWnd::CreateMail(Contact *c, const char *Email, const char *Name) { Mail *m = dynamic_cast(CreateItem(MAGIC_MAIL, NULL, false)); if (m) { bool IsMailTo = false; if (Email) { IsMailTo = _strnicmp(Email, "mailto:", 7) == 0; if (IsMailTo) { Mailto mt(this, Email); mt.Apply(m); } } MailUi *UI = dynamic_cast(m->DoUI()); if (UI) { if (c) { UI->AddRecipient(c); } if (Email && !IsMailTo) { UI->AddRecipient(Email, Name); } } } return m; } Mail *ScribeWnd::LookupMailRef(const char *MsgRef, bool TraceAllUids) { if (!MsgRef) return 0; LAutoString p(NewStr(MsgRef)); char *RawUid = strrchr(p, '/'); if (RawUid) { *RawUid++ = 0; LUri u; LString Uid = u.DecodeStr(RawUid); // Try the mail message map first... Mail *m = Mail::GetMailFromId(Uid); if (m) return m; // Ok, not found, so look in last known folder... ScribeFolder *f = GetFolder(p); if (f) { for (auto t : f->Items) { Mail *m = t->IsMail(); if (m) { auto s = m->GetMessageId(); if (s && !strcmp(s, Uid)) { return m; } if (TraceAllUids) LgiTrace("\t%s\n", s); } } } } return 0; } void ScribeWnd::OnBayesAnalyse(const char *Msg, const char *WhiteListEmail) { LString s, q; s.Printf("
%s
", Msg); if (WhiteListEmail) { q.Printf(LLoadString(IDS_REMOVE_WHITELIST), WhiteListEmail); s += LString("
") + q; } s += ""; LHtmlMsg([&](auto result) { if (result == IDYES) RemoveFromWhitelist(WhiteListEmail); }, this, s, AppName, WhiteListEmail ? MB_YESNO : MB_OK); } bool ScribeWnd::OnBayesResult(const char *MailRef, double Rating) { Mail *m = LookupMailRef(MailRef); if (m) return OnBayesResult(m, Rating); #ifdef _DEBUG else { LgiTrace("%s:%i - error finding mail ref: %s\n", _FL, MailRef); LookupMailRef(MailRef, true); LAssert(!"We should always be able to resolve the reference, unless m is completely deleted"); } #endif return false; } bool ScribeWnd::OnBayesResult(Mail *m, double Rating) { if (!m) return false; LVariant v; GetOptions()->GetValue(OPT_BayesThreshold, v); double BayesThresh = v.CastDouble(); if (BayesThresh < 0.1) BayesThresh = 0.1; if (BayesThresh > 1.0) BayesThresh = 1.0; if (Rating < BayesThresh) { // Not spam, so we continue new mail processing if (m->NewEmail == Mail::NewEmailBayes) { List Nm; Nm.Insert(m); m->NewEmail = Mail::NewEmailGrowl; OnNewMail(&Nm, true); } return false; } // Spam is pink! m->SetMarkColour(Rgb32(255, 0, 0)); m->SetFlags(m->GetFlags() | MAIL_BAYES_SPAM); ScribeBayesianFilterMode FilterMode = BayesOff; if (GetOptions()->GetValue(OPT_BayesFilterMode, v)) FilterMode = (ScribeBayesianFilterMode)v.CastInt32(); if (FilterMode == BayesTrain) { // Move to folder LVariant MoveToPath; if (!GetOptions()->GetValue(OPT_BayesMoveTo, MoveToPath)) { MoveToPath = "/Spam/Probably"; } ScribeFolder *f = GetFolder(MoveToPath.Str()); if (f) { LArray Items; Items.Add(m); f->MoveTo(Items); List obj; obj.Insert(m); OnNewMail(&obj, false); } } else { m->DeleteAsSpam(this); } return true; } static int AccountCmp(ScribeAccount *a, ScribeAccount *b, int Data) { return a->Identity.Sort() - b->Identity.Sort(); } class ScribePasteState : public LProgressDlg { ScribeWnd *App = NULL; ScribeFolder *Folder = NULL; LAutoPtr Data; ssize_t Size = 0; LDataStoreI::StoreTrans Trans; LProgressPane *LoadPane = NULL, *SavePane = NULL; ScribeClipboardFmt *tl = NULL; uint32_t Errors = 0; ssize_t Idx = 0; enum PasteState { LoadingThings, SavingThings, } State = LoadingThings; public: ScribePasteState(ScribeWnd *app, ScribeFolder *folder, LAutoPtr data, ssize_t size) : LProgressDlg(app), App(app), Folder(folder), Data(data), Size(size) { // Paste 'ScribeThingList' tl = (ScribeClipboardFmt*)Data.Get(); Trans = Folder->GetObject()->GetStore()->StartTransaction(); LoadPane = ItemAt(0); LoadPane->SetDescription("Loading objects..."); LoadPane->SetRange(tl->Length()); SavePane = Push(); SavePane->SetRange(tl->Length()); SavePane->SetDescription("Saving: No errors..."); // LProgressDlg will do a SetPulse in it's OnCreate } void OnPulse() { auto Start = LCurrentTime(); static int TimeSlice = 300; //ms if (State == LoadingThings) { while ( Idx < tl->Length() && !IsCancelled() && LCurrentTime() - Start < TimeSlice) { Thing *t = tl->ThingAt(Idx++); if (!t) continue; auto Obj = t->GetObject(); if (Obj->GetInt(FIELD_LOADED) < Store3Loaded) Obj->SetInt(FIELD_LOADED, Store3Loaded); } Value(Idx); if (Idx >= tl->Length()) { State = SavingThings; Idx = 0; } } else if (State == SavingThings) { while ( Idx < tl->Length() && !IsCancelled() && LCurrentTime() - Start < TimeSlice) { Thing *t = tl->ThingAt(Idx++); if (!t) continue; auto Obj = t->GetObject(); LAssert(Obj->GetInt(FIELD_LOADED) == Store3Loaded); // Load loop should have done this already Thing *Dst = App->CreateItem(Obj->Type(), Folder, false); if (Dst) { *Dst = *t; Dst->Update(); if (!Dst->Save(Folder)) { LString s; s.Printf("Saving: " LPrintfSSizeT " error(s)", ++Errors); SetDescription(s); } } else Errors++; } SavePane->Value(Idx); if (Idx >= tl->Length()) { if (Errors > 0) LgiMsg(this, "Failed to save %i of %i objects.", AppName, MB_OK, Errors, tl->Length()); Quit(); return; } } LProgressDlg::OnPulse(); } }; int ScribeWnd::OnCommand(int Cmd, int Event, OsView WndHandle) { // Send mail multi-menu if (Cmd >= IDM_SEND_FROM && Cmd <= IDM_SEND_FROM + (ssize_t)Accounts.Length()) { Send(Cmd - IDM_SEND_FROM); return 0; } // Receive mail multi-menu if (Cmd >= IDM_RECEIVE_FROM && Cmd < IDM_RECEIVE_FROM + (ssize_t)Accounts.Length()) { Receive(Cmd - IDM_RECEIVE_FROM); return 0; } // Preview mail multi-menu if (Cmd >= IDM_PREVIEW_FROM && Cmd < IDM_PREVIEW_FROM + (ssize_t)Accounts.Length()) { Preview(Cmd - IDM_PREVIEW_FROM); return 0; } // Identity multi-menu if (Cmd >= IDM_IDENTITY_BASE && Cmd <= IDM_IDENTITY_BASE + (ssize_t)Accounts.Length()) { SetCurrentIdentity(Cmd - IDM_IDENTITY_BASE - 1); return 0; } // Is this a script tool? if (LScribeScript::Inst && Cmd >= IDM_TOOL_SCRIPT_BASE && Cmd < IDM_TOOL_SCRIPT_BASE + (int)d->Scripts.Length()) { // Do tools menu callback... find the right callback.... LArray c; if (GetScriptCallbacks(LToolsMenu, c)) { for (unsigned i=0; iFunc && c[i]->Param == Cmd) { // Call the callback char Msg[MAX_PATH_LEN]; LScribeScript::Inst->GetLog()->Write ( Msg, sprintf_s(Msg, sizeof(Msg), "\n\nRunning tool script '%s'...\n", c[i]->Script->Code->GetFileName()) ); // Setup the arguments... LScriptArguments Args(NULL); Args.New() = new LVariant((LDom*)this); Args.New() = new LVariant(Cmd); // Call the method ExecuteScriptCallback(*c[i], Args); // Cleanup Args.DeleteObjects(); break; } } } return 0; } // New from template multi-menu if (Cmd >= IDM_NEW_FROM_TEMPLATE && Cmd < IDM_NEW_FROM_TEMPLATE + 100) { int Index = Cmd - IDM_NEW_FROM_TEMPLATE; ScribeFolder *Templates = GetFolder(FOLDER_TEMPLATES); if (Templates) { Templates->LoadThings(); for (auto i: Templates->Items) { Mail *m = i->IsMail(); if (m) { if (Index == 0) { Thing *t = CreateItem(MAGIC_MAIL, 0, false); // GetFolder(FOLDER_OUTBOX) Mail *NewMail = IsMail(t); if (NewMail) { *NewMail = (Thing&)*m; NewMail->DoUI(); break; } } Index--; } } } return 0; } switch (Cmd) { // File menu case IDM_MANAGE_MAIL_STORES: { auto Dlg = new ManageMailStores(this); Dlg->DoModal([this, Dlg](auto dlg, auto id) { LAutoPtr mem(dlg); if (id) { SaveOptions(); if (!UnLoadFolders()) return; LXmlTag *Ms = GetOptions()->LockTag(OPT_MailStores, _FL); if (Ms) { while (Ms->Children.Length()) delete Ms->Children[0]; LXmlTag *t = Dlg->Options.GetChildTag(OPT_MailStores); if (t) { for (auto c: t->Children) { LXmlTag *n = new LXmlTag; n->Copy(*c, true); Ms->InsertTag(n); } } GetOptions()->Unlock(); } LVariant v; GetOptions()->SetValue(OPT_CreateFoldersIfMissing, v = true); if (!Dlg->Options.GetValue(OPT_StartInFolder, v)) v.Empty(); GetOptions()->SetValue(OPT_StartInFolder, v); LoadFolders(NULL); } }); break; } case IDM_REPLICATE: { auto Dlg = new ReplicateDlg(this); Dlg->DoModal([this, Dlg](auto dlg, auto id) { if (id) { UnLoadFolders(); Dlg->StartProcess(); // Don't delete dialog... let it run } else delete dlg; }); break; } case IDM_SECURITY: { // Check for user perm password... // No point allow any old one to edit the security settings. auto ShowDialog = [&]() { auto Dlg = new SecurityDlg(this); Dlg->DoModal(NULL); }; GPassword p; if (p.Serialize(GetOptions(), OPT_UserPermPassword, false)) { GetAccessLevel(this, PermRequireUser, "Security Settings", [&](bool Allow) { if (Allow) ShowDialog(); }); } else { ShowDialog(); } break; } case IDM_OPTIONS: { LVariant ShowTotals; GetOptions()->GetValue(OPT_ShowFolderTotals, ShowTotals); // do the dialog auto Dlg = new OptionsDlg(this); Dlg->DoModal([this, Dlg, ShowTotals](auto dlg, auto id) { if (id) { // set up the POP3 accounts SetupAccounts(); SaveOptions(); // close any IMAP accounts that are now disabled. for (auto a : Accounts) { if (a->Receive.IsConfigured() && a->Receive.IsPersistant()) { if (a->Receive.Disabled()) a->Receive.Disconnect(); else Receive(a->GetIndex()); } } // List/Tree view options update LVariant i; if (GetOptions()->GetValue(OPT_ShowFolderTotals, i) && i.CastInt32() != ShowTotals.CastInt32()) { Tree->UpdateAllItems(); } if (GetOptions()->GetValue(OPT_PreviewLines, i)) { Mail::PreviewLines = i.CastInt32() != 0; } if (MailList) { if (GetOptions()->GetValue(OPT_GridLines, i)) { MailList->DrawGridLines(i.CastInt32() != 0); } MailList->Invalidate(); } // date formats if (GetOptions()->GetValue(OPT_DateFormat, i)) { int Idx = i.CastInt32(); if (Idx >= 0 && Idx < CountOf(DateTimeFormats)) { LDateTime::SetDefaultFormat(DateTimeFormats[Idx]); } } if (GetOptions()->GetValue(OPT_AdjustDateTz, i)) Mail::AdjustDateTz = i.CastInt32() == 0; // SSL debug logging if (GetOptions()->GetValue(OPT_DebugSSL, i)) SslSocket::DebugLogging = i.CastInt32() != 0; // Html edit menu if (GetOptions()->GetValue(OPT_EditControl, i)) { auto mi = Menu->FindItem(IDM_HTML_EDITOR); if (mi) mi->Checked(i.CastInt32() != 0); } } delete dlg; }); break; } case IDM_WORK_OFFLINE: { if (WorkOffline) { WorkOffline->Checked(!WorkOffline->Checked()); LVariant v; GetOptions()->SetValue(OPT_WorkOffline, v = WorkOffline->Checked()); if (!WorkOffline->Checked()) { // Offline -> Online transition. // Check if any pending messages are in the Outbox ScribeFolder *Outbox = GetFolder(FOLDER_OUTBOX); if (Outbox) { bool HasMailToSend = false; for (auto t: Outbox->Items) { Mail *m = t->IsMail(); if (m) { if (TestFlag(m->GetFlags(), MAIL_READY_TO_SEND)) { HasMailToSend = true; break; } } } if (HasMailToSend) { PostEvent(M_COMMAND, IDM_SEND_MAIL, #ifndef __GTK_H__ (LMessage::Param)Handle() #else 0 #endif ); } } } } break; } case IDM_ITEM_FILTER: { if (GetCtrlValue(IDM_ITEM_FILTER)) { if ((SearchView = new LSearchView(this))) { SearchView->Focus(true); SetLayout(); } } else { DeleteObj(SearchView); } ScribeFolder *Folder = GetCurrentFolder(); if (Folder) { Folder->Populate(MailList); } break; } case IDM_PRINT: { if (MailList) { List Sel; if (MailList->LList::GetSelection(Sel)) { for (auto i: Sel) { ThingType *t = dynamic_cast(i); ThingPrint(NULL, t); } } } break; } case IDM_PRINTSETUP: { auto *p = GetPrinter(); if (p && p->Browse(this)) { LString Str; if (p->Serialize(Str, true)) { LVariant v; GetOptions()->SetValue(OPT_PrintSettings, v = Str); } } break; } case IDM_PAGE_SETUP: { auto Dlg = new ScribePageSetup(this, GetOptions()); Dlg->DoModal(NULL); break; } case IDM_EXIT: { LMouse m; GetMouse(m); d->IngoreOnClose = m.Ctrl(); LCloseApp(); break; } // Edit menu case IDM_FIND: { auto v = GetFocus(); LDocView *doc = dynamic_cast(v); if (doc) { doc->DoFind(NULL); } else { ScribeFolder *Folder = GetCurrentFolder(); OpenFinder(this, Folder); } break; } case IDM_COPY: { if (MailList && MailList->Focus()) { List Lst; if (!MailList->GetSelection(Lst)) break; // Copy 'ScribeThingList' ScribeClipboardFmt *tl = ScribeClipboardFmt::Alloc(Lst); if (!tl) break; LClipBoard Clip(this); if (Clip.IsOpen()) { if (!Clip.Binary(d->ClipboardFormat, (uchar*)tl, tl->Sizeof(), true)) { LgiMsg(this, "Couldn't set the clipboard data.", AppName, MB_OK); } } else { LgiMsg(this, "Couldn't open the clipboard.", AppName, MB_OK); } free(tl); } else { LViewI *v = LAppInst->GetFocus(); if (v) v->PostEvent(M_COPY); } break; } case IDM_PASTE: { LViewI *v = LAppInst->GetFocus(); if (v && v->GetWindow() != (LWindow*)this) { v->PostEvent(M_PASTE); break; } if (!MailList->Focus() && !Tree->Focus()) { LgiTrace("%s:%i - List/Tree doesn't have focus.\n"); break; } ScribeFolder *Folder = dynamic_cast(Tree->Selection()); if (!Folder || !Folder->GetObject()) { LgiMsg(this, "No current folder.", AppName, MB_OK); break; } LClipBoard Clip(this); if (!Clip.IsOpen()) { LgiMsg(this, "Couldn't open the clipboard.", AppName, MB_OK); break; } LAutoPtr Data; ssize_t Size = 0; if (!Clip.Binary(d->ClipboardFormat, Data, &Size)) { LgiMsg(this, "Couldn't get the clipboard data.", AppName, MB_OK); break; } if (ScribeClipboardFmt::IsThing(Data.Get(), Size)) { new ScribePasteState(this, Folder, Data, Size); } break; } case IDM_DELETE: { LViewI *f = LAppInst->GetFocus(); LEdit *e = dynamic_cast(f); if (e) { // This handles the case where on a mac the menu eats the delete key, even // when the edit control needs it LKey k(LK_DELETE, 0); k.Down(true); f->OnKey(k); k.Down(false); f->OnKey(k); } else { OnDelete(); } break; } case IDM_DELETE_AS_SPAM: { if (MailList) { List Sel; MailList->GetSelection(Sel); int Index = -1; for (auto i: Sel) { Mail *m = IsMail(i); if (m) { if (Index < 0) { Index = MailList->IndexOf(i); } m->DeleteAsSpam(this); } } if (Index >= 0) { LListItem *i = MailList->ItemAt(Index); if (!i) i = MailList->ItemAt(MailList->Length()-1); if (i) i->Select(true); } } break; } case IDM_REFRESH: { ScribeFolder *f = GetCurrentFolder(); if (!f) break; const char *s = DomToStr(SdRefresh); f->GetFldObj()->OnCommand(s); break; } // Mail menu case IDM_NEW_EMAIL: { CreateMail(); break; } case IDM_SET_READ: case IDM_SET_UNREAD: { ScribeFolder *f = GetCurrentFolder(); if (!f) break; bool SetRead = Cmd == IDM_SET_READ; f->LoadThings(); LArray Change; for (auto t: f->Items) { Mail *m = t->IsMail(); if (m && m->Select()) Change.Add(m->GetObject()); } LVariant v = MAIL_READ; LDataStoreI *Store = f->GetObject()->GetStore(); if (Store->Change(Change, FIELD_FLAGS, v, SetRead ? OpPlusEquals : OpMinusEquals) == Store3Error) { for (auto t : f->Items) { Mail *m = t->IsMail(); if (!m) continue; if (!m->Select()) continue; if (SetRead) m->SetFlags(m->GetFlags() | MAIL_READ); else m->SetFlags(m->GetFlags() & ~MAIL_READ); } } break; } case IDM_REPLY: case IDM_REPLY_ALL: { if (MailList) MailReplyTo(IsMail(MailList->GetSelected()), (Cmd == IDM_REPLY_ALL)); break; } case IDM_FORWARD: { if (MailList) MailForward(IsMail(MailList->GetSelected())); break; } case IDM_BOUNCE: { if (MailList) MailBounce(IsMail(MailList->GetSelected())); break; } case IDM_SEND_MAIL: { Send(); break; } case IDM_RECEIVE_AND_SEND: { d->SendAfterReceive = true; PostEvent(M_COMMAND, IDM_RECEIVE_MAIL, (LMessage::Param)FindControl(IDM_RECEIVE_MAIL)); break; } case IDM_THREAD: { if (MailList) { ScribeFolder *f = GetCurrentFolder(); if (f) { f->SetThreaded(!f->GetThreaded()); f->Populate(MailList); } } break; } case IDM_RECEIVE_ALL: { #define LOG_RECEIVE_ALL 0 int i = 0; Accounts.Sort(AccountCmp); for (auto a : Accounts) { #if LOG_RECEIVE_ALL auto name = a->Identity.Name(); auto email = a->Identity.Email(); LString desc; desc.Printf("%s/%s", name.Str(), email.Str()); #endif if (!a->Receive.IsConfigured()) { #if LOG_RECEIVE_ALL LgiTrace("%s:%i - %i/%s not configured.\n", _FL, a->GetIndex(), desc.Get()); #endif } else if (a->Receive.Disabled() > 0) { #if LOG_RECEIVE_ALL LgiTrace("%s:%i - %i/%s is disabled.\n", _FL, a->GetIndex(), desc.Get()); #endif } else { #if LOG_RECEIVE_ALL LgiTrace("%s:%i - %i/%s will connect.\n", _FL, a->GetIndex(), desc.Get()); #endif Receive(a->GetIndex()); } i++; } break; } case IDM_RECEIVE_MAIL: { LVariant Def; if (GetOptions()->GetValue(OPT_Pop3DefAction, Def) && Def.CastInt32() == 0) return OnCommand(IDM_RECEIVE_ALL, 0, NULL); Receive(0); break; } case IDM_PREVIEW_POP3: { LArray Account; Accounts.Sort(AccountCmp); for (auto a: Accounts) { if (!a->Receive.IsConfigured()) continue; auto Protocol = ProtocolStrToEnum(a->Receive.Protocol().Str()); if (Protocol == ProtocolPop3) { Account.Add(a); break; } } if (Account.Length() == 1) OpenPopView(this, Account); break; } case IDM_CALENDAR: { extern void OpenCalender(ScribeFolder *folder); ScribeFolder *Folder = GetFolder(FOLDER_CALENDAR); if (Folder) { OpenCalender(Folder); } break; } // Contact menu case IDM_NEW_CONTACT: { CreateItem(MAGIC_CONTACT, NULL); break; } case IDM_NEW_GROUP: { CreateItem(MAGIC_GROUP, NULL); break; } // Filter menu case IDM_NEW_FILTER: { Thing *t = CreateItem(MAGIC_FILTER, NULL, false); if (t) { t->IsFilter()->SetIncoming(true); t->DoUI(); } break; } case IDM_FILTER_CURRENT_FOLDER: { ScribeFolder *Folder = GetCurrentFolder(); if (Folder) { List Filters; GetFilters(Filters, false, false, true); List Src; for (auto i: Folder->Items) { if (i->IsMail()) { Src.Insert(i->IsMail()); } } if (!Src[0]) { LgiMsg(this, LLoadString(IDS_NO_MAIL_TO_FILTER), AppName); } else { Filter::ApplyFilters(this, Filters, Src); } } break; } case IDM_FILTER_SELECTION: { ScribeFolder *Folder = GetCurrentFolder(); if (Folder) { List Filters; GetFilters(Filters, false, false, true); List Src; for (auto i: Folder->Items) { if (i->IsMail() && i->Select()) { Src.Insert(i->IsMail()); } } if (Src.Length()) { Filter::ApplyFilters(this, Filters, Src); } } break; } case IDM_DEBUG_FILTERS: { auto i = Menu->FindItem(IDM_DEBUG_FILTERS); if (i) { i->Checked(!i->Checked()); } break; } case IDM_HTML_EDITOR: { auto i = Menu->FindItem(IDM_HTML_EDITOR); if (i) { i->Checked(!i->Checked()); LVariant v; GetOptions()->SetValue(OPT_EditControl, v = i->Checked() ? 1 : 0); } break; } case IDM_FILTERS_DISABLE: { if (d->DisableUserFilters) { d->DisableUserFilters->Checked(!d->DisableUserFilters->Checked()); LVariant v; GetOptions()->SetValue(OPT_DisableUserFilters, v = d->DisableUserFilters->Checked()); } break; } case IDM_BUILD_BAYES_DB: { BuildSpamDb(); break; } case IDM_BAYES_STATS: { BuildStats(); break; } case IDM_BAYES_SETTINGS: { auto Dlg = new BayesDlg(this); Dlg->DoModal([this, Dlg](auto dlg, auto id) { if (id) { LVariant i; if (GetOptions()->GetValue(OPT_BayesFilterMode, i)) { ScribeBayesianFilterMode m = ((ScribeBayesianFilterMode)i.CastInt32()); if (m != BayesOff) { LVariant SpamPath, ProbablyPath; GetOptions()->GetValue(OPT_SpamFolder, SpamPath); GetOptions()->GetValue(OPT_BayesMoveTo, ProbablyPath); if (m == BayesFilter) { ScribeFolder *Spam = GetFolder(SpamPath.Str()); if (!Spam) { LMailStore *RelevantStore = GetMailStoreForPath(SpamPath.Str()); if (RelevantStore) { LString p = SpamPath.Str(); LString::Array a = p.SplitDelimit("/"); Spam = RelevantStore->Root; for (unsigned i=1; iGetSubFolder(a[i]); if (!c) c = Spam->CreateSubDirectory(a[i], MAGIC_MAIL); Spam = c; } } } if (Spam) { LVariant v; GetOptions()->SetValue(OPT_HasSpam, v = 1); } } else if (m == BayesTrain) { ScribeFolder *Probably = GetFolder(ProbablyPath.Str()); if (!Probably) { LgiMsg(this, "Couldn't find the folder '%s'", AppName, MB_OK, ProbablyPath.Str()); } } } } } delete dlg; }); break; } case IDM_BAYES_CHECK: { List Sel; if (MailList) MailList->GetSelection(Sel); for (auto i: Sel) { Thing *t = dynamic_cast(i); if (t) { Mail *m = t->IsMail(); if (m) { d->BayesLog.Empty(); double SpamRating = 0.0; IsSpam(SpamRating, m, true); break; } } } break; } // Tools menu case IDM_SCRIPTING_CONSOLE: case IDM_SHOW_CONSOLE: { ShowScriptingConsole(); if (d->ShowConsoleBtn) d->ShowConsoleBtn->Image(IMG_CONSOLE_NOMSG); break; } case IDM_EXPORT_TEXT_MBOX: { Export_UnixMBox(this); break; } case IDM_IMPORT_CSV: { ImportCsv(this); break; } case IDM_EXPORT_CSV: { ExportCsv(this); break; } case IDM_IMPORT_EML: { ImportEml(this); break; } case IDM_EXPORT_SCRIBE: { - extern void ExportScribe(ScribeWnd *App); - ExportScribe(this); + ExportScribe(this, NULL/* default mail store */); break; } case IDM_IMPORT_TEXT_MBOX: { Import_UnixMBox(this); break; } case IDM_IMP_EUDORA_ADDR: { Import_EudoraAddressBook(this); break; } case IDM_IMP_MOZILLA_ADDR: { Import_MozillaAddressBook(this); break; } case IDM_IMP_MOZILLA_MAIL: { Import_MozillaMail(this); break; } #if WINNATIVE case IDM_IMPORT_OUTLOOK_PAB: { Import_OutlookContacts(this); break; } case IDM_IMPORT_OUTLOOK_ITEMS: { Import_Outlook(this, IMP_OUTLOOK); break; } case IDM_EXPORT_OUTLOOK_ITEMS: { Export_Outlook(this); break; } #endif case IDM_IMP_MBX_EMAIL: { Import_OutlookExpress(this, false); // v4 break; } case IDM_IMP_DBX_EMAIL: { Import_OutlookExpress(this); // v5 break; } case IDM_IMPORT_NS_CONTACTS: { Import_NetscapeContacts(this); break; } case IDM_CHECK_UPDATE: { LVariant v; GetOptions()->GetValue(OPT_SoftwareUpdateIncBeta, v); SoftwareUpdate(this, true, v.CastInt32() != 0, [](auto goingToUpdate) { if (goingToUpdate) LCloseApp(); }); break; } case IDM_LOGOUT: { CurrentAuthLevel = PermRequireNone; auto i = Menu->FindItem(IDM_LOGOUT); if (i) i->Enabled(false); break; } case IDM_LAYOUT1: { LVariant v; int TwoThirds = GetClient().Y() >> 1; GetOptions()->SetValue(OPT_SplitterPos, v = 200); GetOptions()->SetValue(OPT_SubSplitPos, v = TwoThirds); SetLayout(FoldersListAndPreview); break; } case IDM_LAYOUT2: { LVariant v; int TwoThirds = GetClient().Y() >> 1; GetOptions()->SetValue(OPT_SplitterPos, v = TwoThirds); GetOptions()->SetValue(OPT_SubSplitPos, v = 200); SetLayout(PreviewOnBottom); break; } case IDM_LAYOUT3: { LVariant v; GetOptions()->SetValue(OPT_SplitterPos, v = 200); SetLayout(FoldersAndList); break; } case IDM_LAYOUT4: { LVariant v; GetOptions()->SetValue(OPT_SplitterPos, v = 200); GetOptions()->SetValue(OPT_SubSplitPos, v); SetLayout(ThreeColumn); break; } case IDM_CRASH: { int *Crash = 0; *Crash = true; break; } case IDM_DUMP_MEM: { LDumpMemoryStats(0); break; } case IDM_SCRIPT_DEBUG: { LVariant v; if (GetOptions()) GetOptions()->SetValue(OPT_ScriptDebugger, v = true); LVirtualMachine *vm = new LVirtualMachine(d); if (!vm) break; LVmDebugger *dbg = vm->OpenDebugger(); if (!dbg) break; dbg->OwnVm(true); break; } case IDM_SCRIPT_BREAK_ON_WARN: { auto mi = GetMenu()->FindItem(IDM_SCRIPT_BREAK_ON_WARN); if (!mi) break; LVirtualMachine::BreakOnWarning = !mi->Checked(); mi->Checked(LVirtualMachine::BreakOnWarning); break; } // Help menu case IDM_HELP: { LaunchHelp("index.html"); // LgiMsg(this, LLoadString(IDS_ERROR_NO_HELP), AppName, MB_OK); break; } case IDM_FEEDBACK: { LVariant e; if (GetOptions()->GetValue("author", e)) CreateMail(0, e.Str()); else CreateMail(0, AuthorEmailAddr); break; } case IDM_MEMECODE: { LExecute(AuthorHomepage); break; } case IDM_HOMEPAGE: { LVariant hp; if (GetOptions()->GetValue("homepage", hp)) LExecute(hp.Str()); else LExecute(ApplicationHomepage); break; } case IDM_VERSION_HISTORY: { LExecute("http://www.memecode.com/site/ver.php?id=445"); break; } case IDM_DEBUG_INFO: { char s[256]; sprintf_s(s, sizeof(s), "%s#debug", ApplicationHomepage); LExecute(s); break; } case IDM_TUTORIALS: { LExecute("http://www.memecode.com/scribe/tutorials"); break; } case IDM_INSCRIBE_LINK: { LExecute(CommercialHomepage); break; } case IDM_SCRIBE_FAQ: { LExecute(FaqHomepage); break; } case IDM_ABOUT: { extern void ScribeAbout(ScribeWnd *Parent); ScribeAbout(this); break; } default: { if (d->ScriptToolbar) d->ScriptToolbar->ExecuteCallbacks(this, 0, 0, Cmd); break; } } return 0; } void ScribeWnd::OnDelete() { LVariant ConfirmDelete; GetOptions()->GetValue(OPT_ConfirmDelete, ConfirmDelete); if (!ConfirmDelete.CastInt32() || LgiMsg(this, LLoadString(IDS_DELETE_ASK), AppName, MB_YESNO) == IDYES) { LArray Del; if (Tree && Tree->Focus()) { ScribeFolder *Item = dynamic_cast(Tree->Selection()); if (Item) { Tree->OnDelete(Item, false); } } else if (MailList #ifdef MAC && MailList->Focus() #endif ) { List Sel; MailList->GetSelection(Sel); for (auto i: Sel) { Thing *t = dynamic_cast(i); if (t) Del.Add(t->GetObject()); } if (Del.Length()) { auto Store = Del[0]->GetStore(); Store->Delete(Del, true); } else LgiTrace("%s:%i - Nothing to delete\n", _FL); #ifndef MAC MailList->Focus(true); #endif } } } int ScribeWnd::OnNotify(LViewI *Ctrl, LNotification n) { switch (Ctrl->GetId()) { case IDC_THING_LIST: { if (n.Type == LNotifyReturnKey) { LListItem *i = MailList ? MailList->GetSelected() : 0; Thing *t = dynamic_cast(i); if (t) { t->DoUI(); } } else if (n.Type == LNotifyDeleteKey) { /* This is now handled by the menu OnDelete(); return true; */ } if (SearchView && MailList) { SearchView->OnNotify(Ctrl, n); } break; } case IDC_TEXT: { if (PreviewPanel) { PreviewPanel->OnNotify(Ctrl, n); } break; } } return 0; } void ScribeWnd::AddThingSrc(ScribeFolder *src) { if (!d->ThingSources.HasItem(src)) d->ThingSources.Add(src); } void ScribeWnd::RemoveThingSrc(ScribeFolder *src) { d->ThingSources.Delete(src); } LArray ScribeWnd::GetThingSources(Store3ItemTypes Type) { LArray a; for (auto f: d->ThingSources) { if (f->GetItemType() == Type && !f->IsInTrash()) { a.Add(f); } } return a; } bool ScribeWnd::LogFilterActivity() { auto i = Menu->FindItem(IDM_DEBUG_FILTERS); return i ? i->Checked() : false; } bool ScribeWnd::CreateFolders(LAutoString &FileName) { bool Status = false; if (FileName) { char *Ext = LGetExtension(FileName); if (!Ext) { char File[300]; strcpy_s(File, sizeof(File), FileName); strcat(File, ".mail3"); FileName.Reset(NewStr(File)); } // Create objects, and then close the file.. it'll be reloaded later LAutoPtr m(CreateDataStore(FileName, true)); if (m) { m->GetRoot(true); Status = true; } else LgiTrace("%s:%i - CreateDataStore failed.\n", _FL); } else LgiTrace("%s:%i - No file name for CreateFolder.\n", _FL); return Status; } bool ScribeWnd::CompactFolders(LMailStore &Store, bool Interactive) { if (!Store.Store) return false; auto Dlg = new Store3Progress(this, Interactive); Dlg->SetDescription(LLoadString(IDS_CHECKING_OBJECTS)); bool Offline = false; if (WorkOffline) { Offline = WorkOffline->Checked(); WorkOffline->Checked(true); } Store.Store->Compact(this, Dlg, [this, Offline, Dlg](auto status) { LAssert(InThread()); if (WorkOffline) WorkOffline->Checked(Offline); delete Dlg; }); return true; } CalendarSource *CalendarSource::Create(ScribeWnd *App, const char *ObjName, const char *Id) { if (!Stricmp(ObjName, "RemoteCalendarSource")) return new RemoteCalendarSource(App, Id); return new FolderCalendarSource(App, Id); } int ScribeWnd::GetCalendarSources(LArray &Out) { static bool Loaded = false; if (!Loaded) { Loaded = true; CalendarSource::SetCreateIn(NULL); LVariant Create; GetOptions()->GetValue(OPT_CalendarCreateIn, Create); // This should be a list of all calendar folders in ANY mail store... auto CalFlds = GetThingSources(MAGIC_CALENDAR); LXmlTag *t = GetOptions()->LockTag(OPT_CalendarSources, _FL); if (t) { bool AutoPopulate = t->Children.Length() == 0; for (auto c: t->Children) { auto s = CalendarSource::Create(this, c->GetAttr(CalendarSource::OptObject), c->GetTag()); if (s && s->Read()) { // Add known source... if (!Stricmp(Create.Str(), c->GetTag())) { CalendarSource::SetCreateIn(s); } // Remove from CalFlds FolderCalendarSource *Fcs = dynamic_cast(c); if (Fcs) { auto Path = Fcs->GetPath(); for (auto c: CalFlds) { if (c->GetPath().Equals(Path)) { CalFlds.Delete(c); break; } } } } } if (AutoPopulate) { // Now CalFlds should be a list of all calendar folders NOT in the source XML tag for (auto c: CalFlds) { FolderCalendarSource *s = new FolderCalendarSource(this); if (s) { // So add an entry to track it... auto Path = c->GetPath(); s->SetPath(Path); s->SetDisplay(true); s->SetColour(CalendarSource::FindUnusedColour()); s->Write(); } } } GetOptions()->Unlock(); } if (!CalendarSource::GetCreateIn() && CalendarSource::GetSources().Length()) { CalendarSource::SetCreateIn(CalendarSource::GetSources().ItemAt(0)); } } for (unsigned i=0; iPathOption; fi++) { bool Check = true; if (fi->HasOption) { LVariant v; if (GetOptions()->GetValue(fi->HasOption, v)) Check = v.CastInt32() != 0; } if (Check) { ScribeFolder *c = GetFolder(fi->Id); if (c == f) return fi->Id; } } return -1; } ScribeFolder *ScribeWnd::GetCurrentFolder() { if (Tree) { auto *Item = Tree->Selection(); if (Item) { return dynamic_cast(Item); } } return 0; } bool ScribeWnd::GetSystemPath(int Folder, LVariant &Path) { char KeyName[64]; sprintf_s(KeyName, sizeof(KeyName), "Folder-%i", Folder); return GetOptions()->GetValue(KeyName, Path); } LMailStore *ScribeWnd::GetMailStoreForIdentity(const char *IdEmail) { LVariant Tmp; if (!IdEmail) { // Get current identity ScribeAccount *Cur = GetCurrentAccount(); if (Cur) { Tmp = Cur->Identity.Email(); IdEmail = Tmp.Str(); } } if (!IdEmail) return NULL; ScribeAccount *a = NULL; for (auto Acc : Accounts) { LVariant e = Acc->Identity.Email(); if (e.Str() && !_stricmp(e.Str(), IdEmail)) { a = Acc; break; } } if (!a) return NULL; LVariant DestPath = a->Receive.DestinationFolder(); if (!DestPath.Str()) return NULL; return GetMailStoreForPath(DestPath.Str()); } ScribeFolder *ScribeWnd::GetFolder(int Id, LDataI *s) { if (s) { for (auto &f: Folders) if (s->GetStore() == f.Store) return GetFolder(Id, &f); } return GetFolder(Id); } ScribeFolder *ScribeWnd::GetFolder(int Id, LMailStore *Store, bool Quiet) { char KeyName[64]; sprintf_s(KeyName, sizeof(KeyName), "Folder-%i", Id); LVariant FolderName; bool NoOption = false; if (GetOptions()->GetValue(KeyName, FolderName)) { if (ValidStr(FolderName.Str()) && strlen(FolderName.Str()) > 0) { ScribeFolder *c = GetFolder(FolderName.Str(), Store); if (c) { return c; } else if (!Quiet) { LgiTrace("%s:%i - '%s' doesn't exist.\n", _FL, FolderName.Str()); } } } else if (!Quiet) { // LgiTrace("%s:%i - No option '%s'\n", _FL, KeyName); NoOption = true; } switch (Id) { case FOLDER_INBOX: case FOLDER_OUTBOX: case FOLDER_SENT: case FOLDER_TRASH: case FOLDER_CONTACTS: case FOLDER_TEMPLATES: case FOLDER_FILTERS: case FOLDER_CALENDAR: case FOLDER_GROUPS: case FOLDER_SPAM: { ScribeFolder *c = GetFolder(DefaultFolderNames[Id], Store); if (!c) { // if (!Quiet) // LgiTrace("%s:%i - Default folder '%s' doesn't exist.\n", _FL, DefaultFolderNames[Id]); } else if (NoOption) { auto p = c->GetPath(); GetOptions()->SetValue(KeyName, FolderName = p.Get()); } return c; } } return NULL; } bool ScribeWnd::OnMailStore(LMailStore **MailStore, bool Add) { if (!MailStore) { LAssert(!"No mail store pointer?"); return false; } if (Add) { *MailStore = &Folders.New(); if (*MailStore) return true; } else { ssize_t Idx = *MailStore - &Folders[0]; if (Idx >= 0 && Idx < (ssize_t)Folders.Length()) { Folders.DeleteAt(Idx, true); *MailStore = NULL; return true; } else { LAssert(!"Index out of range."); } } return false; } LMailStore *ScribeWnd::GetMailStoreForPath(const char *Path) { if (!Path) return NULL; LToken t(Path, "/"); if (t.Length() > 0) { const char *First = t[0]; // Find the mail store that that t[0] refers to for (unsigned i=0; iGetText(); if (RootStr && !_stricmp(RootStr, First)) { return &Folders[i]; } } } } return NULL; } ScribeFolder *ScribeWnd::GetFolder(const char *Name, LMailStore *s) { ScribeFolder *Folder = 0; if (ValidStr(Name)) { LString Sep("/"); auto t = LString(Name).Split(Sep); LMailStore tmp; LString TmpName; if (t.Length() > 0) { if (!s) { s = GetMailStoreForPath(Name); if (!s) { // IMAP folders? for (auto a: Accounts) { ScribeProtocol Proto = a->Receive.ProtocolType(); if (Proto == ProtocolImapFull) { ScribeFolder *Root = a->Receive.GetRootFolder(); if (Root) { const char *RootStr = Root->GetText(); if (RootStr && a->Receive.GetDataStore() && !_stricmp(RootStr, t[0])) { tmp.Root = Root; tmp.Store = a->Receive.GetDataStore(); s = &tmp; break; } } } } } if (s) { if (*Name == '/') Name++; Name = strchr(Name, '/'); if (!Name) Name = "/"; } } else if (s->Root) { // Check if the store name is on the start of the folder auto RootName = s->Root->GetName(true); if (RootName.Equals(t[0])) { LString::Array a; for (unsigned i=1; iRoot; Folder = s->Root ? s->Root->GetSubFolder(Name) : 0; } } return Folder; } void ScribeWnd::Update(int What) { if (What & UPDATE_TREE) { Tree->Invalidate(); return; } if (What & UPDATE_LIST) { if (MailList) MailList->Invalidate(); return; } } void ScribeWnd::DoDebug(char *s) { } Thing *ScribeWnd::CreateThingOfType(Store3ItemTypes Type, LDataI *obj) { Thing *t = NULL; switch (Type) { case MAGIC_CONTACT: { t = new Contact(this, obj); break; } case MAGIC_MAIL: { t = new Mail(this, obj); break; } case MAGIC_ATTACHMENT: { t = new Attachment(this, obj); break; } case MAGIC_FILTER: { t = new Filter(this, obj); break; } case MAGIC_CALENDAR: { t = new Calendar(this, obj); break; } case MAGIC_GROUP: { t = new ContactGroup(this, obj); break; } default: break; } if (t) { t->App = this; } return t; } void ScribeWnd::GetFilters(List &Filters, bool JustIn, bool JustOut, bool JustInternal) { auto Srcs = GetThingSources(MAGIC_FILTER); for (auto f: Srcs) { for (auto t: f->Items) { Filter *Ftr = t->IsFilter(); if (Ftr) { if (JustIn && !Ftr->GetIncoming()) continue; if (JustOut && !Ftr->GetOutgoing()) continue; if (JustInternal && !Ftr->GetInternal()) continue; Filters.Insert(Ftr); } } } extern int FilterCompare(Filter *a, Filter *b, NativeInt Data); Filters.Sort(FilterCompare); } bool ScribeWnd::ShowToolbarText() { LVariant i; if (GetOptions()->GetValue(OPT_ToolbarText, i)) { return i.CastInt32() != 0; } GetOptions()->SetValue(OPT_ToolbarText, i = true); return true; } void ScribeWnd::HashContacts(LHashTbl,Contact*> &Contacts, ScribeFolder *Folder, bool Deep) { if (!Folder) { // Default item is the contacts folder Folder = GetFolder(FOLDER_CONTACTS); // Also look at all the contact sources... auto Srcs = GetThingSources(MAGIC_CONTACT); for (auto Src: Srcs) { for (auto t: Src->Items) { Contact *c = t->IsContact(); if (!c) continue; auto emails = c->GetEmails(); for (auto e: emails) { if (!Contacts.Find(e)) Contacts.Add(e, c); } } } } // recurse through each folder and make a list // of every contact object we find. if (Folder) { Folder->LoadThings(); for (auto t: Folder->Items) { Contact *c = t->IsContact(); if (c) { auto Emails = c->GetEmails(); for (auto e: Emails) if (e && !Contacts.Find(e)) Contacts.Add(e, c); } } for (auto f = Folder->GetChildFolder(); Deep && f; f = f->GetNextFolder()) { HashContacts(Contacts, f, Deep); } } } List *ScribeWnd::GetEveryone() { return &Contact::Everyone; } bool ScribeWnd::GetContacts(List &Contacts, ScribeFolder *Folder, bool Deep) { LArray Folders; if (!Folder) { Folders = GetThingSources(MAGIC_CONTACT); auto f = GetFolder(FOLDER_CONTACTS); if (f && !Folders.HasItem(f)) Folders.Add(f); } else Folders.Add(Folder); if (!Folders.Length()) return false; for (auto f: Folders) { // recurse through each folder and make a list // of every contact object we find. ScribePerm Perm = f->GetFolderPerms(ScribeReadAccess); bool Safe = CurrentAuthLevel >= Perm; if (Safe) { f->LoadThings(); for (auto t: f->Items) { Contact *c = t->IsContact(); if (c) Contacts.Insert(c); } for (ScribeFolder *c = f->GetChildFolder(); Deep && c; c = c->GetNextFolder()) GetContacts(Contacts, c, Deep); } } return true; } /* This function goes through the database and checks for some basic requirements and fixes things up if they aren't ok. */ bool ScribeWnd::ValidateFolder(LMailStore *s, int Id) { char OptName[32]; sprintf_s(OptName, sizeof(OptName), "Folder-%i", Id); LVariant Path; if (!GetOptions()->GetValue(OptName, Path)) { char Opt[256]; sprintf_s(Opt, sizeof(Opt), "/%s", DefaultFolderNames[Id]); GetOptions()->SetValue(OptName, Path = Opt); } // If the path name has the store name at the start, strip that off... LString Sep("/"); LString::Array Parts = LString(Path.Str()).Split(Sep); if (Parts.Length() > 1) { if (Parts[0].Equals(s->Name)) { Parts.DeleteAt(0, true); Path = Sep.Join(Parts); } else { LMailStore *ms = GetMailStoreForPath(Path.Str()); if (ms) { s = ms; } else { // Most likely the user has renamed something and broken the // path. Lets just error out instead of creating the wrong folder return false; } } } // Now resolve the path... ScribeFolder *Folder = GetFolder(Path.Str(), s); if (!Folder) { char *p = Path.Str(); if (_strnicmp(p, "/IMAP ", 6) != 0) { LAssert(DefaultFolderTypes[Id] != MAGIC_NONE); Folder = s->Root->CreateSubDirectory(*p=='/'?p+1:p, DefaultFolderTypes[Id]); } } if (!Folder) return false; Folder->SetDefaultFields(); return true; } void ScribeWnd::Validate(LMailStore *s) { // Check for all the basic folders int Errors = 0; for (SystemFolderInfo *fi = SystemFolders; fi->PathOption; fi++) { bool Check = true; if (fi->HasOption) { LVariant v; if (GetOptions()->GetValue(fi->HasOption, v)) Check = v.CastInt32() != 0; } if (Check) { if (!ValidateFolder(s, fi->Id)) Errors++; } } if (Errors && LgiMsg(this, "There were errors validating the system folders." "Would you like to review the mail store's system folder paths?", AppName, MB_YESNO) == IDYES) { PostEvent(M_COMMAND, IDM_MANAGE_MAIL_STORES); } } ThingFilter *ScribeWnd::GetThingFilter() { return SearchView; } ScribeAccount *ScribeWnd::GetSendAccount() { LVariant DefSendAcc = 0; if (!GetOptions()->GetValue(OPT_DefaultSendAccount, DefSendAcc)) { for (auto a : Accounts) if (a->Send.Server().Str()) return a; } ScribeAccount *i = Accounts.ItemAt(DefSendAcc.CastInt32()); if (i && i->Send.Server().Str()) return i; return NULL; } LPrinter *ScribeWnd::GetPrinter() { if (!d->PrintOptions) d->PrintOptions.Reset(new LPrinter); return d->PrintOptions; } int ScribeWnd::GetActiveThreads() { int Status = 0; for (auto i: Accounts) { if (i->IsOnline()) { Status++; } } return Status; } class DefaultClientDlg : public LDialog { public: bool DontWarn; DefaultClientDlg(LView *parent) { DontWarn = false; SetParent(parent); LoadFromResource(IDD_WARN_DEFAULT); MoveToCenter(); } int OnNotify(LViewI *Ctrl, LNotification n) { switch (Ctrl->GetId()) { case ID_YES: case ID_NO: { LCheckBox *DW; if (GetViewById(IDC_DONT_WARN, DW)) { DontWarn = DW->Value() != 0; } EndModal(Ctrl->GetId() == ID_YES); break; } } return 0; } }; #if WINNATIVE struct DefaultClient { char DefIcon[MAX_PATH_LEN]; char CmdLine[MAX_PATH_LEN]; char DllPath[MAX_PATH_LEN]; DefaultClient() { auto Exe = LGetExeFile(); sprintf_s(DefIcon, sizeof(DefIcon), "%s,1", Exe.Get()); sprintf_s(CmdLine, sizeof(CmdLine), "\"%s\" /m \"%%1\"", Exe.Get()); LMakePath(DllPath, sizeof(DllPath), Exe, "../ScribeMapi.dll"); } bool IsWindowsXp() { LArray Ver; int Os = LGetOs(&Ver); if ( ( Os == LGI_OS_WIN32 || Os == LGI_OS_WIN64 ) && Ver.Length() > 1 && Ver[0] == 5 && Ver[1] == 1 ) return true; return false; } bool InstallMailto(bool Write) { LAutoPtr mailto = CheckKey(Write, "HKCR\\mailto"); if (!mailto) return false; if (!CheckString(Write, mailto, NULL, "URL:MailTo Protocol")) return false; LAutoPtr deficon = CheckKey(Write, "HKCR\\mailto\\DefaultIcon"); if (!deficon) return false; if (!CheckString(Write, deficon, NULL, DefIcon)) return false; LAutoPtr shell = CheckKey(Write, "HKCR\\mailto\\shell"); if (!shell) return false; if (!CheckString(Write, shell, NULL, "open")) return false; LAutoPtr cmd = CheckKey(Write, "HKCR\\mailto\\shell\\open\\command"); if (!cmd) return false; if (!CheckString(Write, cmd, NULL, CmdLine)) return false; return true; } LAutoPtr CheckKey(bool Write, const char *Key, ...) const { char Buffer[512]; va_list Arg; va_start(Arg, Key); vsprintf_s(Buffer, sizeof(Buffer), Key, Arg); va_end(Arg); LAutoPtr k(new LRegKey(Write, Buffer)); if (k && Write && !k->IsOk()) { if (!k->Create()) { k.Reset(); LgiTrace("%s:%i - Failed to create '%s'\n", _FL, Buffer); } } return k; } bool CheckInt(bool Write, LRegKey *k, const char *Name, uint32_t Value) { if (!k) { LgiTrace("%s:%i - No key: '%s'\n", _FL, Name); return false; } uint32_t Cur; if (!k->GetInt(Name, Cur)) Cur = Value + 1; if (Cur == Value) return true; if (Write) { bool Status = k->SetInt(Name, Value); if (!Status) LgiTrace("%s:%i - Failed to set key '%s': '%s' to %i\n", _FL, k->Name(), Name, Value); return Status; } return false; } bool CheckString(bool Write, LRegKey *k, const char *StrName, const char *StrValue) { if (!k) { LgiTrace("%s:%i - No key: '%s' to '%s'\n", _FL, StrName, StrValue); return false; } LString v; if (k->GetStr(StrName, v)) { bool Same = Stricmp(v.Get(), StrValue) == 0; if (Write && !Same) { bool Status = k->SetStr(StrName, StrValue); if (!Status) LgiTrace("%s:%i - Failed to set key '%s': '%s' to '%s'\n", _FL, k->Name(), StrName, StrValue); return Status; } return Same; } else if (Write) { bool Status = k->SetStr(StrName, StrValue); if (!Status) LgiTrace("%s:%i - Failed to set key '%s': '%s' to '%s'\n", _FL, k->Name(), StrName, StrValue); return Status; } return false; } bool IsDefault() { LAutoPtr mail = CheckKey(false, "HKCU\\Software\\Clients\\Mail"); if (!mail) return false; LString v; if (!mail->GetStr(NULL, v)) return false; return !_stricmp(v, "Scribe"); } bool SetDefault() const { LAutoPtr mail = CheckKey(true, "HKCU\\Software\\Clients\\Mail"); if (!mail) return false; // Set the default client in the current user tree. mail->SetStr(NULL, "Scribe"); // Configure the mailto handler const char *Base = "HKEY_ROOT"; bool Error = false; LRegKey Mt(true, "%s\\mailto", Base); if (Mt.IsOk() || Mt.Create()) { if (!Mt.SetStr(0, "URL:MailTo Protocol") || !Mt.SetStr("URL Protocol", "")) Error = true; } else { LgiTrace("%s:%i - Couldn't open/create registry key (err=%i).\n", _FL, GetLastError()); Error = true; } LRegKey Di(true, "%s\\mailto\\DefaultIcon", Base); if (Di.IsOk() || Di.Create()) { if (!Di.SetStr(0, DefIcon)) Error = true; } else { LgiTrace("%s:%i - Couldn't open/create registry key (err=%i).\n", _FL, GetLastError()); Error = true; } LRegKey c(true, "%s\\mailto\\shell\\open\\command", Base); if (c.IsOk() || c.Create()) { if (!c.SetStr(NULL, CmdLine)) Error = true; } else { LgiTrace("%s:%i - Couldn't open/create registry key (err=%i).\n", _FL, GetLastError()); Error = true; } return Error; } bool InstallAsClient(char *Base, bool Write) { // Create software client entry, to put Scribe in the Internet Options for mail clients. LAutoPtr mail = CheckKey(Write, "%s\\Software\\Clients\\Mail", Base); if (!mail) return false; LAutoPtr app = CheckKey(Write, "%s\\Software\\Clients\\Mail\\Scribe", Base); if (!app) return false; if (!CheckString(Write, app, NULL, AppName)) return false; if (!CheckString(Write, app, "DllPath", DllPath)) return false; LAutoPtr shell = CheckKey(Write, "%s\\Software\\Clients\\Mail\\Scribe\\shell\\open\\command", Base); if (!shell) return false; if (!CheckString(Write, shell, NULL, CmdLine)) return false; LAutoPtr icon = CheckKey(Write, "%s\\Software\\Clients\\Mail\\Scribe\\DefaultIcon", Base); if (!icon) return false; if (!CheckString(Write, icon, NULL, DefIcon)) return false; LAutoPtr proto = CheckKey(Write, "%s\\Software\\Classes\\Protocol\\mailto", Base); if (!proto) return false; if (!CheckString(Write, proto, NULL, "URL:MailTo Protocol")) return false; if (!CheckString(Write, proto, "URL Protocol", "")) return false; if (!CheckInt(Write, proto, "EditFlags", 0x2)) return false; LAutoPtr proto_cmd = CheckKey(Write, "%s\\Software\\Classes\\Protocol\\mailto\\shell\\open\\command", Base); if (!proto_cmd) return false; if (!CheckString(Write, proto_cmd, NULL, CmdLine)) return false; return true; } struct FileType { char *Name; char *Desc; int Icon; }; static FileType FileTypes[]; bool Win7Install(bool Write) { // http://msdn.microsoft.com/en-us/library/windows/desktop/cc144154%28v=vs.85%29.aspx LArray Ver; int Os = LGetOs(&Ver); if ( ( Os == LGI_OS_WIN32 || Os == LGI_OS_WIN64 ) && Ver[0] >= 6) { char Path[MAX_PATH_LEN]; auto Exe = LGetExeFile(); for (int i=0; FileTypes[i].Name; i++) { LAutoPtr base = CheckKey(Write, "HKEY_CLASSES_ROOT\\%s", FileTypes[i].Name); if (!base) return false; if (!CheckString(Write, base, NULL, FileTypes[i].Desc)) return false; LAutoPtr r = CheckKey(Write, "HKEY_CLASSES_ROOT\\%s\\shell\\Open\\command", FileTypes[i].Name); if (!r) return false; sprintf_s(Path, sizeof(Path), "\"%s\" -u \"%%1\"", Exe.Get()); if (!CheckString(Write, r, NULL, Path)) return false; LAutoPtr ico = CheckKey(Write, "HKEY_CLASSES_ROOT\\%s\\DefaultIcon", FileTypes[i].Name); if (!ico) return false; sprintf_s(Path, sizeof(Path), "%s,%i", Exe.Get(), FileTypes[i].Icon); if (!CheckString(Write, ico, NULL, Path)) return false; } LAutoPtr r = CheckKey(Write, "HKEY_LOCAL_MACHINE\\SOFTWARE\\Clients\\Mail\\Scribe\\Capabilities"); if (!r) return false; if (!CheckString(Write, r, "ApplicationDescription", "Scribe is a small lightweight email client.") && !CheckString(Write, r, "ApplicationName", "Scribe") && !CheckString(Write, r, "ApplicationIcon", DefIcon)) return false; LAutoPtr as = CheckKey(Write, "HKEY_LOCAL_MACHINE\\SOFTWARE\\Clients\\Mail\\Scribe\\Capabilities\\FileAssociations"); if (!as) return false; if (!CheckString(Write, as, ".eml", "Scribe.Email") && !CheckString(Write, as, ".msg", "Scribe.Email") && !CheckString(Write, as, ".mbox", "Scribe.Folder") && !CheckString(Write, as, ".mbx", "Scribe.Folder") && !CheckString(Write, as, ".ics", "Scribe.Calendar") && !CheckString(Write, as, ".vcs", "Scribe.Calendar") && !CheckString(Write, as, ".vcf", "Scribe.Contact") && !CheckString(Write, as, ".mail3", "Scribe.MailStore")) return false; LAutoPtr ua = CheckKey(Write, "HKEY_LOCAL_MACHINE\\SOFTWARE\\Clients\\Mail\\Scribe\\Capabilities\\UrlAssociations"); if (!ua) return false; if (!CheckString(Write, ua, "mailto", "Scribe.Mailto")) return false; LAutoPtr a = CheckKey(Write, "HKEY_LOCAL_MACHINE\\SOFTWARE\\RegisteredApplications"); if (!a) return false; if (!CheckString(Write, a, "Scribe", "SOFTWARE\\Clients\\Mail\\Scribe\\Capabilities")) return false; } return true; } void Win7Uninstall() { for (int i=0; FileTypes[i].Name; i++) { LRegKey base(true, "HKEY_CLASSES_ROOT\\%s", FileTypes[i].Name); base.DeleteKey(); } } }; DefaultClient::FileType DefaultClient::FileTypes[] = { { "Scribe.Email", "Email", 2 }, { "Scribe.Folder", "Mailbox", 0 }, { "Scribe.Calendar", "Calendar Event", 6 }, { "Scribe.Contact", "Contact", 4 }, { "Scribe.MailStore", "Mail Store", 0 }, { "Scribe.Mailto", "Mailto Protocol", 0 }, { 0, 0 } }; #endif void ScribeWnd::SetDefaultHandler() { #if WINNATIVE if (LAppInst->GetOption("noreg")) return; LVariant RegisterClient; if (!GetOptions()->GetValue(OPT_RegisterWindowsClient, RegisterClient)) RegisterClient = true; if (!RegisterClient.CastInt32()) return; // Create IE mail client entries for local machine and current user DefaultClient Def; bool OldAssert = LRegKey::AssertOnError; LRegKey::AssertOnError = false; bool RegistryOk = ( !Def.IsWindowsXp() || Def.InstallMailto(true) ) && Def.InstallAsClient("HKLM", true) && Def.Win7Install(true); LRegKey::AssertOnError = OldAssert; if (!RegistryOk) { // Need write permissions to fix up the registry? NeedsCapability("RegistryWritePermissions"); return; } // Check if the user wants us to be the default client LVariant n = true; GetOptions()->GetValue(OPT_CheckDefaultEmail, n); if (n.CastInt32()) { // HKEY_CURRENT_USER\Software\Microsoft\Windows\Shell\Associations\UrlAssociations\mailto\UserChoice LRegKey::AssertOnError = false; bool IsDef = Def.IsDefault(); if (!IsDef) { // Ask the user... auto Dlg = new DefaultClientDlg(this); Dlg->DoModal([this, Dlg, Def, OldAssert](auto dlg, auto id) { if (id) { auto Error = !Def.SetDefault(); LVariant v; GetOptions()->SetValue(OPT_CheckDefaultEmail, v = (int) (!Dlg->DontWarn)); OnSetDefaultHandler(Error, OldAssert); } delete dlg; }); } else OnSetDefaultHandler(false, OldAssert); } #endif } void ScribeWnd::OnSetDefaultHandler(bool Error, bool OldAssert) { #if WINDOWS LRegKey::AssertOnError = OldAssert; #endif if (Error) NeedsCapability("RegistryWritePermissions"); } void ScribeWnd::OnSelect(List *l, bool ChangeEvent) { Mail *m = (l && l->Length() == 1) ? (*l)[0]->IsMail() : 0; if (Commands) { bool NotCreated = m && !TestFlag(m->GetFlags(), MAIL_CREATED); Commands->SetCtrlEnabled(IDM_DELETE, l && l->Length() > 0); Commands->SetCtrlEnabled(IDM_DELETE_AS_SPAM, l && l->Length() > 0); Commands->SetCtrlEnabled(IDM_PRINT, l && l->Length() == 1); Commands->SetCtrlEnabled(IDM_REPLY, NotCreated); Commands->SetCtrlEnabled(IDM_REPLY_ALL, NotCreated); Commands->SetCtrlEnabled(IDM_FORWARD, m != 0); Commands->SetCtrlEnabled(IDM_BOUNCE, m != 0); } if (PreviewPanel && GetEffectiveLayoutMode() != 3) { if (!PreviewPanel->IsAttached()) { SetItemPreview(PreviewPanel); } Thing *t = (l && l->Length() == 1) ? (*l)[0] : 0; PreviewPanel->OnThing(t, ChangeEvent); /* if (d->Debug) d->Debug->OnThing(t); */ } } class SpellErrorInst { public: int Id; LString Word; LString::Array Suggestions; SpellErrorInst(int id) { Id = id; // Decor = LCss::TextDecorSquiggle; // DecorColour.Rgb(255, 0, 0); } ~SpellErrorInst() { } bool OnMenu(LSubMenu *m) { if (Suggestions.Length()) { for (unsigned i=0; iAppendItem(Suggestions[i], 100 + i, true); } m->AppendSeparator(); } char Buf[256]; sprintf_s(Buf, sizeof(Buf), LLoadString(IDS_ADD_TO_DICTIONARY, "Add '%s' to dictionary"), Word.Get()); m->AppendItem(Buf, 1, true); return true; } void OnMenuClick(int i) { if (i == 1) { // Add to dictionary... /* if (PostThreadEvent(SpellHnd, M_ADD_WORD, (LMessage::Param) new LString(Word))) { // FIXME LAssert(!"Impl me."); // View->PostEvent(M_DELETE_STYLE, (LMessage::Param) dynamic_cast(this)); } */ } else if (i >= 100 && i < 100 + (int)Suggestions.Length()) { // Change spelling.. char *Replace = Suggestions[i - 100]; if (Replace) { char16 *w = Utf8ToWide(Replace); if (w) { /* int NewLen = StrlenW(w); if (NewLen > Len) { // Bigger... memcpy(View->NameW() + Start, w, Len * sizeof(char16)); View->Insert(Start + Len, w + Len, NewLen - Len); } else if (NewLen < Len) { // Smaller... memcpy(View->NameW() + Start, w, NewLen * sizeof(char16)); View->Delete(Start + NewLen, Len - NewLen); } else { // Just copy... memcpy(View->NameW() + Start, w, Len * sizeof(char16)); RefreshLayout(Start, Len); } */ DeleteArray(w); } } } } }; class MailTextView : public LTextView3 { ScribeWnd *App; LSpellCheck *Thread; LColour c[8]; LHashTbl, SpellErrorInst*> ErrMap; SpellErrorInst *NewErrorInst() { int Id; while (ErrMap.Find(Id = LRand(10000))) ; SpellErrorInst *Inst = new SpellErrorInst(Id); if (!Inst) return NULL; ErrMap.Add(Id, Inst); return Inst; } public: MailTextView(ScribeWnd *app, int Id, int x, int y, int cx, int cy, LFontType *FontType) : LTextView3(Id, x, y, cx, cy, FontType) { App = app; Thread = 0; int i=0; c[i++].Rgb(0x80, 0, 0); c[i++].Rgb(0, 0x80, 0); c[i++].Rgb(0, 0, 0x80); c[i++].Rgb(0x80, 0x80, 0); c[i++].Rgb(0x80, 0, 0x80); c[i++].Rgb(0, 0x80, 0x80); c[i++].Rgb(0x80, 0x80, 0x80); c[i++].Rgb(0xc0, 0xc0, 0xc0); for (i=0; i 0 && !StrchrW(SpellDelim, Text[Start-1])) Start--; if (Len > 0) { // Text being added Len += Origin - Start; while ((ssize_t)Start + Len < Size && !StrchrW(SpellDelim, Text[Start + Len])) Len++; } else if (Len < 0) { // Text being deleted Len = Origin - Start; while ((ssize_t)Start + Len < Size && !StrchrW(SpellDelim, Text[Start + Len])) Len++; } if (!Thread) Thread = App->GetSpellThread(); if (Thread && Len > 0) { LString Str(Text+Start, Len); LArray Params; Thread->Check(AddDispatch(), Str, Start, Len, &Params); } // Adjust all the positions of the styles after this. for (auto s = Style.begin(); s != Style.end(); ) { if (s->Start >= Origin && s->Owner == 1) { if (Length < 0 && s->Start < Origin - Length) { // In the deleted text... Style.Delete(s); continue; } // After the deleted text s->Start += Length; LAssert(s->Start >= 0); } s++; } } } void PourText(size_t Start, ssize_t Len) { LTextView3::PourText(Start, Len); for (auto l: Line) { int n=0; char16 *t = Text + l->Start; char16 *e = t + l->Len; while ((*t == ' ' || *t == '>') && t < e) if (*t++ == '>') n++; if (n > 0) l->c = c[(n-1)%CountOf(c)]; } } LMessage::Result OnEvent(LMessage *m) { switch (m->Msg()) { case M_CHECK_TEXT: { LAutoPtr Ct((LSpellCheck::CheckText*)m->A()); if (!Ct || !Thread) break; // Clear existing spelling error styles ssize_t Start = Ct->Start; ssize_t End = Start + Ct->Len; for (auto i = Style.begin(); i != Style.end(); ) { if (i->End() < (size_t)Start || i->Start >= End) { // Outside the area we are re-styling. i++; } else { if (i->Owner == STYLE_SPELLING) { // Existing error style inside the area Style.Delete(i); } else { // Existing non-error style... i++; } } } // Insert the new styles for (auto Ct: Ct->Errors) { SpellErrorInst *ErrInst = NewErrorInst(); LAutoPtr Style(new LTextView3::LStyle(STYLE_SPELLING)); if (Style && ErrInst) { Style->View = this; Style->Start = Ct.Start; Style->Len = Ct.Len; Style->Font = GetFont(); Style->Data = ErrInst->Id; Style->DecorColour = LColour::Red; Style->Decor = LCss::TextDecorSquiggle; ErrInst->Word = LString(Text + Style->Start, Style->End()); ErrInst->Suggestions = Ct.Suggestions; InsertStyle(Style); } } // Update the screen... Invalidate(); break; } case M_DELETE_STYLE: { /* LTextView3::LStyle *s = (LTextView3::LStyle*)m->A(); if (s && Style.HasItem(s)) { Style.Delete(s); Invalidate(); } else LAssert(0); */ break; } } return LTextView3::OnEvent(m); } bool OnStyleClick(LStyle *style, LMouse *m) { switch (style->Owner) { case STYLE_URL: { if (m->Left() && m->Down() && m->Double()) { LString s(Text + style->Start, style->Len); LUri u(s); if ( (u.sProtocol && !_stricmp(u.sProtocol, "mailto")) || LIsValidEmail(s) ) { Mailto m(App, s); Mail *email = App->CreateMail(); if (email) { m.Apply(email); email->DoUI(); return true; } } else { // Web link? LExecute(s); } } break; } default: return false; } return true; } }; LDocView *ScribeWnd::CreateTextControl(int Id, const char *MimeType, bool Editor, Mail *m) { LDocView *Ctrl = 0; // Get the default font LFontType FontType; bool UseFont = FontType.Serialize(GetOptions(), OPT_EditorFont, false); if (Editor) { if (MimeType && !_stricmp(MimeType, sTextHtml)) { // Use the built in html editor LRichTextEdit *Rte; if ((Ctrl = Rte = new LRichTextEdit(Id))) { if (UseFont) Ctrl->SetFont(FontType.Create(), true); // Give the control the speller settings: LVariant Check, Lang, Dict; if (GetOptions()->GetValue(OPT_SpellCheck, Check) && Check.CastInt32() != 0) { if (GetOptions()->GetValue(OPT_SpellCheckLanguage, Lang)) Rte->SetValue(LDomPropToString(SpellCheckLanguage), Lang); if (GetOptions()->GetValue(OPT_SpellCheckDictionary, Dict)) Rte->SetValue(LDomPropToString(SpellCheckDictionary), Dict); // Set the spell thread: LSpellCheck *t = GetSpellThread(); if (t) Rte->SetSpellCheck(t); } } } else { // Use the built in plain text editor Ctrl = new MailTextView(this, Id, 0, 0, 200, 200, (UseFont) ? &FontType : 0); } } else { // Create a view only control for the mime type: LDocView *HtmlCtrl = NULL; if (!MimeType || _stricmp(MimeType, sTextPlain) == 0) Ctrl = new MailTextView(this, Id, 0, 0, 200, 200, (UseFont) ? &FontType : 0); #if 0 // defined(WINDOWS) && !defined(__GTK_H__) else if (_stricmp(MimeType, sApplicationInternetExplorer) == 0) HtmlCtrl = Ctrl = CreateIeControl(Id); #endif else HtmlCtrl = Ctrl = new Html1::LHtml(Id, 0, 0, 200, 200); if (HtmlCtrl && UseFont) { LVariant LoadImg; if (GetOptions()->GetValue(OPT_HtmlLoadImages, LoadImg)) HtmlCtrl->SetLoadImages(LoadImg.CastInt32() != 0); HtmlCtrl->SetFont(FontType.Create(), true); } } if (Ctrl) { Ctrl->SetUrlDetect(true); Ctrl->SetAutoIndent(true); LVariant WrapOption; if (GetOptions()->GetValue(OPT_WordWrap, WrapOption)) { if (WrapOption.CastInt32()) { LVariant WrapCols = 80; GetOptions()->GetValue(OPT_WrapAtColumn, WrapCols); Ctrl->SetWrapAtCol(WrapCols.CastInt32()); } else { Ctrl->SetWrapAtCol(0); } } } return Ctrl; } void ScribeWnd::GrowlInfo(LString title, LString text) { LGrowl *g = d->GetGrowl(); if (!g) return; LAutoPtr n(new LGrowl::LNotify); n->Name = "info"; n->Title = title; n->Text = text; g->Notify(n); } void ScribeWnd::GrowlOnMail(Mail *m) { LVariant v; LAutoPtr n(new LGrowl::LNotify); n->Name = "new-mail"; n->Title = m->GetSubject(); int Len = 64; char sLen[16]; sprintf_s(sLen, sizeof(sLen), "%i", Len); if (m->GetVariant("BodyAsText", v, sLen)) { char *s = v.Str(); if (s) { int Words = 0; bool Lut[256]; memset(Lut, 0, sizeof(Lut)); Lut[(int)' '] = Lut[(int)'\t'] = Lut[(int)'\r'] = Lut[(int)'\n'] = true; char *c; for (c = s; *c && Words < 30; ) { while (*c && Lut[(int)*c]) c++; while (*c && !Lut[(int)*c]) c++; Words++; } n->Text.Set(s, c - s); } } LGrowl *g = d->GetGrowl(); if (g) { g->Notify(n); m->NewEmail = Mail::NewEmailTray; } } void ScribeWnd::OnNewMailSound() { static uint64 PrevTs = 0; auto Now = LCurrentTime(); if (Now - PrevTs > 30000) { PrevTs = Now; LVariant v; if (GetOptions()->GetValue(OPT_NewMailSoundFile, v) && LFileExists(v.Str())) { LPlaySound(v.Str(), SND_ASYNC); } } } void ScribeWnd::OnFolderSelect(ScribeFolder *f) { if (SearchView) SearchView->OnFolder(); } void ScribeWnd::OnNewMail(List *MailObjs, bool Add) { if (!MailObjs) return; LVariant v; bool ShowDetail = MailObjs->Length() < 5; List NeedsFiltering; LArray NeedsBayes; LArray NeedsGrowl; LArray Resort; for (auto m: *MailObjs) { if (Add) { #if DEBUG_NEW_MAIL LgiTrace("%s:%i - NewMail.OnNewMail t=%p, uid=%s, mode=%s\n", _FL, (Thing*)m, m->GetServerUid().ToString().Get(), toString(m->NewEmail)); #endif switch (m->NewEmail) { case Mail::NewEmailNone: { auto Loaded = m->GetLoaded(); #if DEBUG_NEW_MAIL LgiTrace("%s:%i - NewMail.OnNewMail.GetLoaded=%i uid=%s\n", _FL, (int)Loaded, m->GetServerUid().ToString().Get()); #endif if (Loaded != Store3Loaded) { LOG_STORE("\tOnNewMail calling SetLoaded.\n"); m->SetLoaded(); m->NewEmail = Mail::NewEmailLoading; } else { m->NewEmail = Mail::NewEmailFilter; LOG_STORE("\tOnNewMail none->NeedsFiltering.\n"); NeedsFiltering.Insert(m); } break; } case Mail::NewEmailLoading: { auto Loaded = m->GetLoaded(); if (Loaded == Store3Loaded) { m->NewEmail = Mail::NewEmailFilter; NeedsFiltering.Insert(m); if (m->GetFolder() && !Resort.HasItem(m->GetFolder())) { Resort.Add(m->GetFolder()); } } break; } case Mail::NewEmailFilter: { NeedsFiltering.Insert(m); break; } case Mail::NewEmailBayes: { NeedsBayes.Add(m); break; } case Mail::NewEmailGrowl: { if (d->Growl) { NeedsGrowl.Add(m); break; } else { m->NewEmail = Mail::NewEmailTray; // no Growl loaded so fall through to new tray mail } } case Mail::NewEmailTray: { LAssert(m->GetObject()); Mail::NewMailLst.Insert(m); OnNewMailSound(); break; } default: { LAssert(!"Hmmm what happen?"); break; } } } else { #if DEBUG_NEW_MAIL LgiTrace("%s:%i - NewMail.OnNewMail.RemoveNewMail t=%p, uid=%s\n", _FL, (Thing*)m, m->GetServerUid().ToString().Get()); #endif Mail::NewMailLst.Delete(m); if (m->NewEmail == Mail::NewEmailFilter) m->NewEmail = Mail::NewEmailNone; } } if (Add) { // Do filtering if (NeedsFiltering.Length()) { List Filters; if (!GetOptions()->GetValue(OPT_DisableUserFilters, v) || !v.CastInt32()) { GetFilters(Filters, true, false, false); } if (Filters.Length() > 0) { // Run the filters #if DEBUG_NEW_MAIL LgiTrace("%s:%i - NewMail.OnNewMail.Filtering %i mail through %i filters\n", _FL, (int)NeedsFiltering.Length(), (int)Filters.Length()); #endif Filter::ApplyFilters(NULL, Filters, NeedsFiltering); // All the email not filtered now needs to be sent to the bayes filter. for (auto m: NeedsFiltering) { if (m->NewEmail == Mail::NewEmailBayes) { #if DEBUG_NEW_MAIL LgiTrace("%s:%i - NewMail.OnNewMail.NeedsBayes t=%p, msgid=%s\n", _FL, (Thing*)m, m->GetMessageId()); #endif NeedsBayes.Add(m); } else if (m->NewEmail == Mail::NewEmailGrowl) { #if DEBUG_NEW_MAIL LgiTrace("%s:%i - NewMail.OnNewMail.NeedsGrowl t=%p, msgid=%s\n", _FL, (Thing*)m, m->GetMessageId()); #endif NeedsGrowl.Add(m); } } } } // Do bayes if (NeedsBayes.Length()) { ScribeBayesianFilterMode FilterMode = BayesOff; if (GetOptions()->GetValue(OPT_BayesFilterMode, v)) FilterMode = (ScribeBayesianFilterMode)v.CastInt32(); for (unsigned i=0; iMailMessageIdMap(); // Start the Bayesian rating process off Store3Status Status = IsSpam(Rating, m); if (Status == Store3Success) { // Bayes done... this stops OnBayesResult from passing it back to OnNewMail m->NewEmail = Mail::NewEmailGrowl; // Process bayes result if (!OnBayesResult(m, Rating)) { // Not spam... so on to growl #if DEBUG_NEW_MAIL LgiTrace("%s:%i - NewMail.Bayes.NeedsGrowl t=%p, msgid=%s\n", _FL, (Thing*)m, m->GetMessageId()); #endif NeedsGrowl.Add(m); } else { // Is spam... do nothing... #if DEBUG_NEW_MAIL LgiTrace("%s:%i - NEW_MAIL: Bayes->IsSpam t=%p, msgid=%s\n", _FL, (Thing*)m, m->GetMessageId()); #endif m->NewEmail = Mail::NewEmailNone; m = 0; } } else { // Didn't get classified immediately, so it'll be further // processed when OnBayesResult gets called later. } } else { // Bayes filter not active... move it to growl m->NewEmail = Mail::NewEmailGrowl; NeedsGrowl.Add(m); } } } if (NeedsGrowl.Length()) { if (d->Growl) { if (!ShowDetail) { LAutoPtr n(new LGrowl::LNotify); n->Name = "new-mail"; n->Title = "New Mail"; n->Text.Printf("%i new messages", (int)MailObjs->Length()); d->Growl->Notify(n); } else { for (unsigned i=0; iGetLoaded(); LAssert(state == Store3Loaded); // If loaded then notify GrowlOnMail(m); } } } for (unsigned i=0; iNewEmail = Mail::NewEmailTray; #if DEBUG_NEW_MAIL LgiTrace("%s:%i - NewMail.OnNewMail.Growl->Tray t=%p, msgid=%s\n", _FL, (Thing*)m, m->GetMessageId()); #endif LAssert(m->GetObject()); Mail::NewMailLst.Insert(m); OnNewMailSound(); } } if (GetOptions()->GetValue(OPT_NewMailNotify, v) && v.CastInt32()) { PostEvent(M_SCRIBE_NEW_MAIL); } for (unsigned i=0; iGetPath(); LgiTrace("%s:%i - NewMail.OnNewMail.Resort=%s\n", _FL, Path.Get()); #endif Resort[i]->ReSort(); } } } LColour ScribeWnd::GetColour(int i) { static LColour MailPreview; static LColour UnreadCount; #define ReadColDef(Var, Tag, Default) \ case Tag: \ { \ if (!Var.IsValid()) \ { \ Var = Default; \ LColour::GetConfigColour("Colour."#Tag, Var); \ } \ return Var; \ break; \ } switch (i) { ReadColDef(MailPreview, L_MAIL_PREVIEW, LColour(0, 0, 255)); ReadColDef(UnreadCount, L_UNREAD_COUNT, LColour(0, 0, 255)); default: { return LColour((LSystemColour)i); break; } } return LColour(); } bool WriteXmlTag(LStream &p, LXmlTag *t) { const char *Tag = t->GetTag(); bool ValidTag = ValidStr(Tag) && !IsDigit(Tag[0]); if (ValidTag) p.Print("<%s", Tag); else { LAssert(0); return false; } LXmlTree Tree; static const char *EncodeEntitiesAttr = "\'<>\"\n"; for (unsigned i=0; iAttr.Length(); i++) { auto &a = t->Attr[i]; // Write the attribute name p.Print(" %s=\"", a.GetName()); // Encode the value if (!Tree.EncodeEntities(&p, a.GetValue(), -1, EncodeEntitiesAttr)) { LAssert(0); return false; } // Write the delimiter p.Write((void*)"\"", 1); if (iAttr.Length()-1 /*&& TestFlag(d->Flags, GXT_PRETTY_WHITESPACE)*/) { p.Write((void*)"\n", 1); } } p.Write(">", 1); return true; } LString ScribeWnd::ProcessReplyForwardTemplate(Mail *m, Mail *r, char *Xml, int &Cursor, const char *MimeType) { LStringPipe p(256); if (m && r && Xml) { bool IsHtml = MimeType && !_stricmp(MimeType, sTextHtml); LMemStream mem(Xml, strlen(Xml)); LXmlTag x; LXmlTree t(GXT_KEEP_WHITESPACE | GXT_NO_DOM); if (t.Read(&x, &mem, 0)) { ScribeDom Dom(this); Dom.Email = m; if (IsHtml) { const char *EncodeEntitiesContent = "\'<>\""; for (auto Tag: x.Children) { if (!WriteXmlTag(p, Tag)) { break; } for (const char *c = Tag->GetContent(); c; ) { const char *s = strstr(c, "") : NULL; if (s && e) { if (s > c) { t.EncodeEntities(&p, (char*)c, s - c, EncodeEntitiesContent); } s += 2; LString Var = LString(s, e - s).Strip(); LVariant v; if (Var) { LString::Array parts = Var.SplitDelimit(" "); if (parts.Length() > 0) { if (Dom.GetValue(parts[0], v)) { for (unsigned mod = 1; mod < parts.Length(); mod++) { LString::Array m = parts[mod].SplitDelimit("=", 1); if (m.Length() == 2) { if (m[0].Equals("quote")) { LVariant Quote; if (Dom.GetValue(m[1], Quote)) { LVariant WrapColumn; if (!GetOptions()->GetValue(OPT_WrapAtColumn, WrapColumn) || WrapColumn.CastInt32() <= 0) WrapColumn = 76; WrapAndQuote(p, Quote.Str(), WrapColumn.CastInt32(), v.Str(), NULL, MimeType); v.Empty(); } } } } switch (v.Type) { case GV_STRING: { p.Push(v.Str()); break; } case GV_DATETIME: { p.Push(v.Value.Date->Get()); break; } case GV_NULL: break; default: { LAssert(!"Unsupported type."); break; } } } } } c = e + 2; } else { p.Print("%s", c); break; } } } } else { LArray Tags; Tags.Add(&x); for (auto Tag: x.Children) { Tags.Add(Tag); } for (unsigned i=0; iGetTag() && Dom.GetValue(Tag->GetTag(), v)) { char *s = v.Str(); if (s) { const char *Quote; if ((Quote = Tag->GetAttr("quote"))) { LVariant q, IsQuote; GetOptions()->GetValue(OPT_QuoteReply, IsQuote); if (r->GetValue(Quote, q)) { Quote = q.Str(); } else { Quote = "> "; } if (Quote && IsQuote.CastInt32()) { LVariant WrapColumn; if (!GetOptions()->GetValue(OPT_WrapAtColumn, WrapColumn) || WrapColumn.CastInt32() <= 0) WrapColumn = 76; WrapAndQuote(p, Quote, WrapColumn.CastInt32(), s); } else { p.Push(s); } } else { p.Push(s); } } else if (v.Type == GV_DATETIME && v.Value.Date) { char b[64]; v.Value.Date->Get(b, sizeof(b)); p.Push(b); } } else if (Tag->IsTag("cursor")) { int Size = (int)p.GetSize(); char *Buf = new char[Size+1]; if (Buf) { p.Peek((uchar*)Buf, Size); Buf[Size] = 0; RemoveReturns(Buf); Cursor = LCharLen(Buf, "utf-8"); DeleteArray(Buf); } } if (Tag->GetContent()) { p.Push(Tag->GetContent()); } } } } } return p.NewGStr(); } LAutoString ScribeWnd::ProcessSig(Mail *m, char *Xml, const char *MimeType) { LStringPipe p; if (!m || !Xml) return LAutoString(); if (MimeType && !_stricmp(MimeType, sTextHtml)) p.Write(Xml, strlen(Xml)); else { LMemStream mem(Xml, strlen(Xml)); LXmlTag x; LXmlTree t(GXT_KEEP_WHITESPACE|GXT_NO_DOM); if (t.Read(&x, &mem, 0)) { for (auto Tag: x.Children) { if (Tag->IsTag("random-line")) { char *FileName = 0; if ((FileName = Tag->GetAttr("Filename"))) { char *File = LReadTextFile(FileName); if (File) { LToken Lines(File, "\r\n"); DeleteArray(File); char *RandomLine = Lines[LRand((unsigned)Lines.Length())]; if (RandomLine) { p.Push(RandomLine); } } } } else if (Tag->IsTag("random-paragraph")) { char *FileName = 0; if ((FileName = Tag->GetAttr("Filename"))) { char *File = LReadTextFile(FileName); if (File) { List Para; for (char *f=File; f && *f; ) { // skip whitespace while (strchr(" \t\r\n", *f)) f++; if (*f) { char *Start = f; char *n; while ((n = strchr(f, '\n'))) { f = n + 1; if (f[1] == '\n' || (f[1] == '\r' && f[2] == '\n')) { break; } } if (f == Start) f += strlen(f); Para.Insert(NewStr(Start, f-Start)); } } DeleteArray(File); char *RandomPara = Para.ItemAt(LRand((int)Para.Length())); if (RandomPara) { p.Push(RandomPara); } Para.DeleteArrays(); } } } else if (Tag->IsTag("include-file")) { char *FileName = 0; if ((FileName = Tag->GetAttr("filename"))) { char *File = LReadTextFile(FileName); if (File) { p.Push(File); DeleteArray(File); } } } else if (Tag->IsTag("quote-file")) { char *FileName = 0; char *QuoteStr = 0; if ((FileName = Tag->GetAttr("filename")) && (QuoteStr = Tag->GetAttr("Quote"))) { } } else { p.Push(Tag->GetContent()); } } } } return LAutoString(p.NewStr()); } // Get the effective permissions for a resource. // // This method can be used by both sync and async code: // In sync mode, don't supply a callback (ie = NULL) and the return value will be: // Store3Error - no access // Store3Delayed - no access, asking the user for password // Store3Success - allow immediate access // // In async mode, supply a callback and wait for the response. // callback(false) - no access // callback(true) - allow immediate access // in this mode the same return values as sync mode are used. Store3Status ScribeWnd::GetAccessLevel(LViewI *Parent, ScribePerm Required, const char *ResourceName, std::function Callback) { if (Required >= CurrentAuthLevel) { if (Callback) Callback(true); return Store3Success; } if (!Parent) Parent = this; switch (Required) { default: break; case PermRequireUser: { bool Status = false; GPassword p; if (!p.Serialize(GetOptions(), OPT_UserPermPassword, false)) { if (Callback) Callback(true); return Store3Success; } char Msg[256]; sprintf_s(Msg, sizeof(Msg), LLoadString(IDS_ASK_USER_PASS), ResourceName); auto d = new LInput(Parent, "", Msg, AppName, true); d->DoModal([this, d, p, Callback](auto dlg, auto id) { if (id && d->GetStr()) { char Pass[256]; p.Get(Pass); bool Status = strcmp(Pass, d->GetStr()) == 0; if (Status) { CurrentAuthLevel = PermRequireUser; auto i = Menu->FindItem(IDM_LOGOUT); if (i) i->Enabled(true); if (Callback) Callback(true); } else { if (Callback) Callback(false); } } delete dlg; }); return Store3Delayed; } case PermRequireAdmin: { LString Key; Key.Printf("Scribe.%s", OPT_AdminPassword); auto Hash = LAppInst->GetConfig(Key); if (ValidStr(Hash)) { if (Callback) Callback(false); return Store3Error; } uchar Bin[256]; ssize_t BinLen = 0; if ((BinLen = ConvertBase64ToBinary(Bin, sizeof(Bin), Hash, strlen(Hash))) != 16) { LgiMsg(Parent, "Admin password not correctly encoded.", AppName); if (Callback) Callback(false); return Store3Error; } auto d = new LInput(Parent, "", LLoadString(IDS_ASK_ADMIN_PASS), AppName, true); d->DoModal([this, d, Bin, Callback](auto dlg, auto id) { if (id && d->GetStr()) { unsigned char Digest[16]; char Str[256]; sprintf_s(Str, sizeof(Str), "%s admin", d->GetStr().Get()); MDStringToDigest(Digest, Str); if (memcmp(Bin, Digest, 16) == 0) { CurrentAuthLevel = PermRequireAdmin; auto i = Menu->FindItem(IDM_LOGOUT); if (i) i->Enabled(true); if (Callback) Callback(true); } else { if (Callback) Callback(false); } } delete dlg; }); return Store3Delayed; } } if (Callback) Callback(true); return Store3Success; } void ScribeWnd::GetAccountSettingsAccess(LViewI *Parent, ScribeAccessType AccessType, std::function Callback) { LVariant Level = (int)PermRequireNone; // Check if user level access is required char *Opt = (char*)(AccessType == ScribeReadAccess ? OPT_AccPermRead : OPT_AccPermWrite); GetOptions()->GetValue(Opt, Level); /* // Check if admin access is required char *Admin = GetScribeAccountPerm((char*) (AccessType == ScribeReadAccess ? "Read" : "Write")); if (Admin && _stricmp(Admin, "Admin") == 0) { Level = PermRequireAdmin; } */ GetAccessLevel(Parent ? Parent : this, (ScribePerm)Level.CastInt32(), "Account Settings", Callback); } LMutex *ScribeWnd::GetLock() { return _Lock; } void ScribeWnd::OnBeforeConnect(ScribeAccount *Account, bool Receive) { if (Receive) { Account->Receive.Enabled(false); } else { Account->Send.Enabled(true); } if (StatusPanel) { StatusPanel->Invalidate(); } } void ScribeWnd::OnAfterConnect(ScribeAccount *Account, bool Receive) { if (Account) { SaveOptions(); if (ScribeState == ScribeExiting) { LCloseApp(); } } if (Receive) { Account->Receive.Enabled(true); } else { Account->Send.Enabled(true); } if (StatusPanel) { StatusPanel->Invalidate(); } if (d->SendAfterReceive) { bool Online = false; for (auto a: Accounts) { bool p = a->Receive.IsPersistant(); bool o = a->Receive.IsOnline(); if (!p && o) { Online = true; break; } } if (!Online) { Send(-1, true); d->SendAfterReceive = false; } } } void ScribeWnd::Send(int Which, bool Quiet) { if (ScribeState == ScribeExiting) return; if (Which < 0) { LVariant v; if (GetOptions()->GetValue(OPT_DefaultSendAccount, v)) Which = v.CastInt32(); } LArray Outboxes; unsigned i; for (i=0; iLoadThings(); Outboxes.Add(OutBox); } } if (Outboxes.Length() < 1) { LgiMsg(this, LLoadString(IDS_NO_OUTGOING_FOLDER), AppName, MB_OK); return; } int MailToSend = 0; List Acc; SendAccountlet *Default = 0; { // Create list of accounts for (auto a: Accounts) { Acc.Insert(&a->Send); a->Send.Outbox.DeleteObjects(); if (Which < 0 || a->GetIndex() == Which) Default = &a->Send; } // If the default it not in the list try the first one.. if (!Default) Default = Acc[0]; } for (i=0; iItems) { Mail *m = t->IsMail(); if (!m) continue; uint32_t Flags = m->GetFlags(); if (!TestFlag(Flags, MAIL_SENT) && TestFlag(Flags, MAIL_READY_TO_SEND)) { - GDataIt To = m->GetObject()->GetList(FIELD_TO); + LDataIt To = m->GetObject()->GetList(FIELD_TO); if (To && To->Length()) { LAutoPtr Out(new ScribeEnvelope); if (Out) { if (m->OnBeforeSend(Out)) { SendAccountlet *Send = 0; for (auto a: Acc) { LVariant Ie = a->GetAccount()->Identity.Email(); if (ValidStr(Ie.Str()) && a->OnlySendThroughThisAccount()) { if (Ie.Str() && m->GetFromStr(FIELD_EMAIL) && _stricmp(Ie.Str(), m->GetFromStr(FIELD_EMAIL)) == 0) { Send = a; break; } } } if (!Send) { Send = Default; } if (Send) { LAssert(Out->To.Length() > 0); Out->SourceFolder = OutBox->GetPath(); Send->Outbox.Add(Out.Release()); MailToSend++; } } } } else { LgiMsg( this, LLoadString(IDS_ERROR_NO_RECIPIENTS), AppName, MB_OK, m->GetSubject() ? m->GetSubject() : (char*)"(none)"); } } } } if (MailToSend) { for (auto a: Acc) { if (a->Outbox.Length() > 0 && !a->IsOnline()) { if (a->IsConfigured()) { a->Connect(0, Quiet); } else { auto d = new LAlert(this, AppName, LLoadString(IDS_ERROR_NO_CONFIG_SEND), LLoadString(IDS_CONFIGURE), LLoadString(IDS_CANCEL)); d->DoModal([this, d, a](auto dlg, auto id) { if (id == 1) a->GetAccount()->InitUI(this, 1, NULL); delete dlg; }); } } } } else { LgiMsg(this, LLoadString(IDS_NO_MAIL_TO_SEND), AppName, MB_OK, Outboxes[0]->GetText()); } } void ScribeWnd::Receive(int Which) { #define LOG_RECEIVE 0 if (ScribeState == ScribeExiting) { LgiTrace("%s:%i - Won't receive, is trying to exit.\n", _FL); return; } for (ScribeAccount *i: Accounts) { if (i->GetIndex() != Which) continue; if (i->Receive.IsOnline()) { #if LOG_RECEIVE LgiTrace("%s:%i - %i already online.\n", _FL, Which); #endif } else if (i->Receive.Disabled() > 0) { #if LOG_RECEIVE LgiTrace("%s:%i - %i is disabled.\n", _FL, Which); #endif } else if (!i->Receive.IsConfigured()) { #if LOG_RECEIVE LgiTrace("%s:%i - %i is not configured.\n", _FL, Which); #endif auto a = new LAlert(this, AppName, LLoadString(IDS_ERROR_NO_CONFIG_RECEIVE), LLoadString(IDS_CONFIGURE), LLoadString(IDS_CANCEL)); a->DoModal([this, a, i](auto dlg, auto id) { if (id == 1) i->InitUI(this, 2, NULL); delete dlg; }); } else { i->Receive.Connect(0, false); } break; } } bool ScribeWnd::GetHelpFilesPath(char *Path, int PathSize) { const char *Index = "index.html"; char Install[MAX_PATH_LEN]; strcpy_s(Install, sizeof(Install), ScribeResourcePath()); for (int i=0; i<5; i++) { char p[MAX_PATH_LEN]; LMakePath(p, sizeof(p), Install, "Help"); LMakePath(p, sizeof(p), p, Index); LgiTrace("Trying '%s'\n", p); if (LFileExists(p)) { LTrimDir(p); strcpy_s(Path, PathSize, p); return true; } #ifdef MAC LMakePath(p, sizeof(p), Install, "Resources/Help"); LMakePath(p, sizeof(p), p, Index); // LgiTrace("Trying '%s'\n", p); if (LFileExists(p)) { LTrimDir(p); strcpy_s(Path, PathSize, p); return true; } #endif LTrimDir(Install); // Try all the parent folders... } LArray Ext; LArray Help; Ext.Add("index.html"); LRecursiveFileSearch(Install, &Ext, &Help); for (unsigned i=0; iSetEvents(d); return Browse->SetUri(Path); } #else #ifdef MAC if (LExecute(Path)) return true; #else if (Hash) *Hash = '#'; // Get browser... char Browser[256]; if (!LGetAppForMimeType("application/browser", Browser, sizeof(Browser))) { LgiTrace("%s:%i - LGetAppForMimeType('text/html') failed.\n", _FL); goto HelpError; } // Execute browser to view help... char Uri[256]; sprintf_s(Uri, sizeof(Uri), "\"file://%s\"", Path); #ifdef WIN32 char *c; while (c = strchr(Uri, '\\')) *c = '/'; #endif LgiTrace("LaunchHelp('%s','%s').\n", Browser, Uri); if (!LExecute(Browser, Uri)) { LgiTrace("%s:%i - LExecute('%s','%s') failed.\n", _FL, Browser, Uri); goto HelpError; } return true; #endif #endif HelpError: LgiMsg(this, LLoadString(IDS_ERROR_NO_HELP), AppName, MB_OK); } return false; } void ScribeWnd::Preview(int Which) { LArray a; a.Add(Accounts[Which]); OpenPopView(this, a); } bool MergeSegments(LDataPropI *DstProp, LDataPropI *SrcProp, LDom *Dom) { LDataI *Src = dynamic_cast(SrcProp); LDataI *Dst = dynamic_cast(DstProp); if (!Dst || !Src) return false; Store3MimeType Mt(Src->GetStr(FIELD_MIME_TYPE)); if (Mt.IsText()) { // Set the headers... Dst->SetStr(FIELD_INTERNET_HEADER, Src->GetStr(FIELD_INTERNET_HEADER)); // Do mail merge of data part... LAutoStreamI Data = Src->GetStream(_FL); if (Data) { // Read data out into a string string... LAutoString Str(new char[(int)Data->GetSize()+1]); Data->Read(Str, (int)Data->GetSize()); Str[Data->GetSize()] = 0; // Do field insert and save result to segment char *Merged = ScribeInsertFields(Str, Dom); LAutoStreamI Mem(new LMemStream(Merged, strlen(Merged))); Dst->SetStream(Mem); } } else { // Straight copy... Dst->CopyProps(*Src); } // Merge children segments as well - GDataIt Sc = Src->GetList(FIELD_MIME_SEG); - GDataIt Dc = Dst->GetList(FIELD_MIME_SEG); + LDataIt Sc = Src->GetList(FIELD_MIME_SEG); + LDataIt Dc = Dst->GetList(FIELD_MIME_SEG); if (Dc && Sc) { for (unsigned i=0; iLength(); i++) { // Create new dest child, and merge the source child across LDataPropI *NewDestChild = Dc->Create(Dst->GetStore()); if (!MergeSegments(NewDestChild, (*Sc)[i], Dom)) return false; LDataI *NewDest = dynamic_cast(NewDestChild); if (NewDest) NewDest->Save(Dst); } } return true; } // Either 'FileName' or 'Source' will be valid void ScribeWnd::MailMerge(LArray &Contacts, const char *FileName, Mail *Source) { ScribeFolder *Outbox = GetFolder(FOLDER_OUTBOX); if (Outbox && Contacts.Length() && (FileName || Source)) { LAutoPtr ImportEmail; if (FileName) { LAutoPtr File(new LFile); if (File->Open(FileName, O_READ)) { Thing *t = CreateItem(MAGIC_MAIL, Outbox, false); ImportEmail.Reset(Source = t->IsMail()); if (!Source->Import(Source->AutoCast(File), sMimeMessage)) { Source = 0; } } } if (Source) { List Msgs; ScribeDom Dom(this); // Do the merging of the document with the database for (unsigned i=0; iGetContact() : 0; Contact *temp = new Contact(this); if (!c) { temp->SetFirst(la->sName); temp->SetEmail(la->sAddr); c = temp; } Dom.Con = c; Thing *t = CreateItem(MAGIC_MAIL, Outbox, false); if (t) { Dom.Email = t->IsMail(); LAutoString s; if (s.Reset(ScribeInsertFields(Source->GetSubject(), &Dom))) Dom.Email->SetSubject(s); LDataPropI *Recip = Dom.Email->GetTo()->Create(Dom.Email->GetObject()->GetStore()); if (Recip) { Recip->CopyProps(*Dom.Con->GetObject()); Recip->SetInt(FIELD_CC, 0); Dom.Email->GetTo()->Insert(Recip); } MergeSegments( Dom.Email->GetObject()->GetObj(FIELD_MIME_SEG), Source->GetObject()->GetObj(FIELD_MIME_SEG), &Dom); Msgs.Insert(Dom.Email); } temp->DecRef(); } // Ask user what to do if (Msgs[0]) { char Msg[256]; sprintf_s(Msg, sizeof(Msg), LLoadString(IDS_MAIL_MERGE_Q), Msgs.Length()); auto Ask = new LAlert(this, AppName, Msg, LLoadString(IDS_SAVE_TO_OUTBOX), LLoadString(IDS_CANCEL)); Ask->DoModal([this, Msgs, Outbox](auto dlg, auto id) { switch (id) { case 1: // Save To Outbox { for (size_t i=0; iSave(Outbox); } break; } case 2: // Cancel { for (size_t i=0; iOnDelete(); } break; } } delete dlg; }); } else { LgiMsg(this, LLoadString(IDS_MAIL_MERGE_EMPTY), AppName); } } } } ScribeFolder *CastFolder(LDataI *f) { if (!f) { LAssert(!"Null pointer"); return 0; } if (f->Type() == MAGIC_FOLDER) { return (ScribeFolder*)f->UserData; } return 0; } Thing *CastThing(LDataI *t) { if (!t) { LAssert(!"Null pointer"); return 0; } if (t->Type() == MAGIC_FOLDER) { LAssert(!"Shouldn't be a folder"); return 0; } return (Thing*)t->UserData; } LString _GetUids(LArray &items) { LString::Array a; for (auto i: items) a.New().Printf(LPrintfInt64, i->GetInt(FIELD_SERVER_UID)); return LString(",").Join(a); } /// Received new items from a storage backend. void ScribeWnd::OnNew( /// The parent folder of the new item LDataFolderI *Parent, /// All the new items LArray &NewItems, /// The position in the parent folder or -1 int Pos, /// Non-zero if the object is a new email. bool IsNew) { ScribeFolder *Fld = CastFolder(Parent); int UnreadDiff = 0; if (Stricmp(Parent->GetStr(FIELD_FOLDER_NAME), "Contacts") && Stricmp(Parent->GetStr(FIELD_FOLDER_NAME), "Calendar")) { LOG_STORE("OnNew(%s, %s, %i, %i)\n", Parent->GetStr(FIELD_FOLDER_NAME), _GetUids(NewItems).Get(), Pos, IsNew); } if (!Fld) { // When this happens the parent hasn't been loaded by the UI thread // yet, so if say the IMAP back end notices a new sub-folder then we // can safely ignore it until such time as the UI loads the parent // folder. At which point the child we got told about here will be // loaded anyway. #if DEBUG_NEW_MAIL LDataFolderI *p = dynamic_cast(Parent); LgiTrace("%s:%i - NewMail.OnNew no UI object for '%s'\n", _FL, p ? p->GetStr(FIELD_FOLDER_NAME) : NULL); #endif return; } if (!Parent || !NewItems.Length()) { LAssert(!"Param error"); return; } for (auto cb: d->Store3EventCallbacks) cb->OnNew(Parent, NewItems, Pos, IsNew); List NewMail; for (auto Item: NewItems) { if (Item->Type() == MAGIC_FOLDER) { // Insert new folder into the right point on the tree... LDataFolderI *SubObject = dynamic_cast(Item); if (!SubObject) { LAssert(!"Not a valid folder."); continue; } ScribeFolder *Sub = CastFolder(SubObject); // LgiTrace("OnNew '%s', Sub=%p Children=%i\n", SubObject->GetStr(FIELD_FOLDER_NAME), Sub, SubObject->SubFolders().Length()); if (!Sub) { // New folder... if ((Sub = new ScribeFolder)) { Sub->App = this; Sub->SetObject(SubObject, false, _FL); Fld->Insert(Sub); } } else { // Existing folder... Fld->Insert(Sub, Pos); } } else { Thing *t = CastThing(Item); if (!t) { // Completely new thing... t = CreateThingOfType((Store3ItemTypes) Item->Type(), Item); } if (t) { if (t->DeleteOnAdd.Obj) { // Complete a delayed move auto OldFolder = t->App->GetFolder(t->DeleteOnAdd.Path); if (!OldFolder) { LgiTrace("%s:%i - Couldn't resolve old folder '%s'\n", _FL, t->DeleteOnAdd.Path.Get()); } else { auto OldItem = t->DeleteOnAdd.Obj; if (!OldFolder->Items.HasItem(OldItem)) LgiTrace("%s:%i - Couldn't find old obj.\n", _FL); else OldItem->OnDelete(); } } t->SetParentFolder(Fld); Fld->Update(); if (Fld->Select()) { // Existing thing... t->SetFieldArray(Fld->GetFieldArray()); ThingFilter *Filter = GetThingFilter(); if (!Filter || Filter->TestThing(t)) { MailList->Insert(t, -1, false); } } Mail *m = t->IsMail(); if (m) { // LgiTrace("OnNew %p\n", m->GetObject()); UnreadDiff += TestFlag(m->GetFlags(), MAIL_READ) ? 0 : 1; #if DEBUG_NEW_MAIL LgiTrace("%s:%i - NewMail.OnNew t=%p uid=%s IsNew=%i\n", _FL, t, m->GetServerUid().ToString().Get(), IsNew); #endif if (IsNew) { LAssert(!NewMail.HasItem(m)); NewMail.Insert(m); } } t->Update(); } } } if (UnreadDiff) Fld->OnUpdateUnRead(UnreadDiff, false); if (MailList && Fld->Select()) MailList->Sort(ListItemCompare, (NativeInt)Fld); if (NewMail.Length()) OnNewMail(&NewMail); } void ScribeWnd::OnPropChange(LDataStoreI *store, int Prop, LVariantType Type) { switch (Prop) { case FIELD_IS_ONLINE: { // This is in case we receive a message after the app has shutdown and // deleted 'this'. if (ScribeState > ScribeRunning) break; if (StatusPanel) StatusPanel->Invalidate(); for (auto a : Accounts) { if (a->Receive.GetDataStore() == store) { int64 Online = store->GetInt(FIELD_IS_ONLINE); auto Old = ScribeState; // This prevents the folders unloading during this call. // Which causes crashes. ScribeState = ScribeLoadingFolders; a->Receive.OnOnlineChange(Online != 0); ScribeState = Old; break; } } break; } } } void ScribeWnd::SetContext(const char *file, int line) { d->CtxFile = file; d->CtxLine = line; } bool ScribeWnd::OnChange(LArray &items, int FieldHint) { bool UpdateSelection = false; List NewMail; ScribeFolder *Parent = 0; // LOG_STORE("OnChange(%i, %i)\n", (int)items.Length(), FieldHint); LAssert(d->CtxFile != NULL); for (unsigned c=0; cStore3EventCallbacks.Length(); c++) { d->Store3EventCallbacks[c]->OnChange(items, FieldHint); } for (unsigned i=0; iType() == MAGIC_FOLDER) { auto ItemFolder = dynamic_cast(Item); ScribeFolder *fld = CastFolder(ItemFolder); if (fld) { if (FieldHint == FIELD_STATUS) { // This is the delayed folder load case: fld->IsLoaded(true); if (fld->Select()) fld->Populate(GetMailList()); } else { fld->Update(); } } else { // LAssert(!"Can't cast to folder?"); } } else if ((t = CastThing(Item))) { ThingUi *Ui = t->GetUI(); if (Ui) Ui->OnChange(); Mail *m = t->IsMail(); if (m) { auto StoreFlags = Item->GetInt(FIELD_FLAGS); #if 0 LgiTrace("App.OnChange(%i) handler %p: %s -> %s\n", FieldHint, m, EmailFlagsToStr(m->FlagsCache).Get(), EmailFlagsToStr(StoreFlags).Get()); #endif if (TestFlag(m->FlagsCache, MAIL_NEW) && !TestFlag(StoreFlags, MAIL_NEW)) { Mail::NewMailLst.Delete(m); Parent = m->GetFolder(); } if (m->FlagsCache != StoreFlags) { // LgiTrace("%s:%i - OnChange mail flags changed.\n", _FL); m->SetFlagsCache(StoreFlags, false, false); Parent = m->GetFolder(); } if (m->NewEmail == Mail::NewEmailLoading) { auto Loaded = m->GetLoaded(); if (Loaded < Store3Loaded) { #if DEBUG_NEW_MAIL LgiTrace("%s:%i - NewMail.OnChange.GetBody t=%p, uid=%s, mode=%s, loaded=%s (%s:%i)\n", _FL, (Thing*)m, m->GetServerUid().ToString().Get(), toString(m->NewEmail), toString(Loaded), d->CtxFile, d->CtxLine); #endif m->GetBody(); } else { #if DEBUG_NEW_MAIL LgiTrace("%s:%i - NewMail.OnChange.NewMail t=%p, uid=%s, mode=%s, loaded=%s (%s:%i)\n", _FL, (Thing*)m, m->GetServerUid().ToString().Get(), toString(m->NewEmail), toString(Loaded), d->CtxFile, d->CtxLine); #endif NewMail.Insert(m); } } } else if (t->IsCalendar()) { for (auto cv: CalendarView::CalendarViews) cv->OnContentsChanged(); } if (FieldHint != FIELD_FLAGS) { // Call the on load handler... t->IsLoaded(true); } if (t->GetList()) { t->Update(); if (FieldHint != FIELD_FLAGS) UpdateSelection |= t->Select(); } } } if (MailList && UpdateSelection) { List Sel; if (MailList->GetSelection(Sel)) { OnSelect(&Sel, true); } } if (Parent) Parent->OnUpdateUnRead(0, true); if (NewMail.Length()) OnNewMail(&NewMail); d->CtxFile = NULL; d->CtxLine = 0; return true; } ContactGroup *ScribeWnd::FindGroup(char *SearchName) { ScribeFolder *g = GetFolder(FOLDER_GROUPS); if (!g || !SearchName) return 0; g->LoadThings(); for (Thing *t: g->Items) { ContactGroup *Grp = t->IsGroup(); if (!Grp) continue; auto Name = Grp->GetObject()->GetStr(FIELD_GROUP_NAME); if (Name) { if (!_stricmp(Name, SearchName)) { return Grp; } } } return 0; } bool ScribeWnd::Match(LDataStoreI *Store, LDataPropI *Address, int ObjType, LArray &Matches) { if (!Store || !Address) return 0; // char *Addr = Address->GetStr(ObjType == MAGIC_CONTACT ? FIELD_EMAIL : FIELD_NAME); auto Addr = Address->GetStr(FIELD_EMAIL); if (!Addr) return 0; List *l = GetEveryone(); if (!l) return 0; Matches.Length(0); if (ObjType == MAGIC_CONTACT) { for (auto c: *l) { if (strchr(Addr, '@')) { auto Emails = c->GetEmails(); for (auto e: Emails) { if (e.Equals(Addr)) { Matches.Add(c); break; } } } } } else if (ObjType == MAGIC_GROUP) { ScribeFolder *g = GetFolder(FOLDER_GROUPS); if (!g) return false; g->LoadThings(); for (auto t: g->Items) { ContactGroup *Grp = t->IsGroup(); if (!Grp) continue; List GrpAddr; if (!Grp->GetAddresses(GrpAddr)) continue; for (auto a: GrpAddr) { if (_stricmp(a, Addr) == 0) { Matches.Add(t); break; } } GrpAddr.DeleteArrays(); } } return Matches.Length() > 0; } bool ScribeWnd::OnMove(LDataFolderI *new_parent, LDataFolderI *old_parent, LArray &items) { if (!new_parent || items.Length() == 0) return false; bool Status = false; for (auto cb: d->Store3EventCallbacks) cb->OnMove(new_parent, old_parent, items); ScribeFolder *New = CastFolder(new_parent); ScribeFolder *Old = old_parent ? CastFolder(old_parent) : NULL; if (New) { ssize_t SelIdx = -1; for (unsigned n=0; nType()) { case MAGIC_FOLDER: { ScribeFolder *i = CastFolder(items[n]); if (i) { i->Detach(); New->Insert(i); Status = true; } break; } default: { Thing *t = CastThing(items[n]); if (t) { int UnreadMail = t->IsMail() ? !TestFlag(t->IsMail()->GetFlags(), MAIL_READ) : false; if (Old && UnreadMail) Old->OnUpdateUnRead(-1, false); if (t->GetList()) { if (t->Select() && SelIdx < 0) SelIdx = t->GetList()->IndexOf(t); t->GetList()->Remove(t); } // This closes the user interface if the object is being moved to the trash... if (New->GetSystemFolderType() == Store3SystemTrash) t->SetUI(); t->SetParentFolder(New); if (New->Select()) { MailList->Insert(t); MailList->ReSort(); } if (UnreadMail) New->OnUpdateUnRead(1, false); if (New->GetItemType() == MAGIC_ANY && t->IsMail()) { Mail::NewMailLst.Delete(t->IsMail()); } Status = true; } break; } } } if (MailList && SelIdx >= 0 && MailList->Length() > 0) { if (SelIdx >= (ssize_t)MailList->Length()) SelIdx = MailList->Length()-1; MailList->Value(SelIdx); } } return Status; } bool ScribeWnd::OnDelete(LDataFolderI *Parent, LArray &Items) { int UnreadAdjust = 0; int SelectIdx = -1; LOG_STORE("OnDelete(%s, %i)\n", Parent->GetStr(FIELD_FOLDER_NAME), (int)Items.Length()); if (!Items.Length()) return false; for (unsigned c=0; cStore3EventCallbacks.Length(); c++) { d->Store3EventCallbacks[c]->OnDelete(Parent, Items); } ScribeFolder *Fld = NULL; for (unsigned i=0; iType() == MAGIC_FOLDER) { // Insert new folder into the right point on the tree... ScribeFolder *Sub = CastFolder(Item); if (!Sub) return true; LgiTrace("OnDelete folder %p (%i)\n", (ThingType*)Sub, ThingType::DirtyThings.HasItem(Sub)); // Remove the deleted object from the dirty queue... ThingType::DirtyThings.Delete(Sub); // And make sure it can't get dirty again... Sub->SetWillDirty(false); // Remove all the children LDataIterator &SubItems = Sub->GetFldObj()->Children(); LArray Children; for (LDataI *c = SubItems.First(); c; c = SubItems.Next()) { Children.Add(c); } OnDelete(Sub->GetFldObj(), Children); // Remove the UI element Sub->Remove(); // Free the memory for the object DeleteObj(Sub); } else { Fld = Parent ? CastFolder(Parent) : 0; if (!Fld) return false; Thing *t = CastThing(Item); if (!t) { // This happens when the item moves from one store to another // of a different type and a copy of the old object is made. The // 'Thing' is detached from 'Item' and attached to the new LDataI // object. // However if the object is an unread email... we should still decrement the // folder's unread count. if (Item->Type() == MAGIC_MAIL && !(Item->GetInt(FIELD_FLAGS) & MAIL_READ)) { Fld->OnUpdateUnRead(-1, false); } } else { LAssert(!Fld || Fld == t->GetFolder()); // LgiTrace("OnDelete thing %p (%i)\n", (ThingType*)t, ThingType::DirtyThings.HasItem(t)); // Remove the deleted object from the dirty queue... ThingType::DirtyThings.Delete(t); // And make sure it can't get dirty again... t->SetWillDirty(false); // Was the thing currently being previewed? if (PreviewPanel && PreviewPanel->GetCurrent() == t) PreviewPanel->OnThing(0, false); // Was the object selected in the thing list? if (Fld && Fld->Select()) { // If so, select an adjacent item. if (SelectIdx < 0 && t->Select()) SelectIdx = MailList->IndexOf(t); MailList->Remove(t); } // Was the object an unread email? Mail *m = t->IsMail(); if (m) { if (!TestFlag(m->GetFlags(), MAIL_READ) && Fld) UnreadAdjust--; Mail::NewMailLst.Delete(m); } t->SetUI(); t->SetParentFolder(NULL); t->SetObject(NULL, false, _FL); if (Fld) Fld->Update(); if (!t->DecRef()) { t->SetDirty(false); t->SetWillDirty(false); } } } } if (Fld) Fld->OnUpdateUnRead(UnreadAdjust, false); if (SelectIdx >= 0) { LListItem *i = MailList->ItemAt(SelectIdx); if (!i && MailList->Length() > 0) i = MailList->ItemAt(MailList->Length()-1); if (i) i->Select(true); } return true; } bool ScribeWnd::AddStore3EventHandler(LDataEventsI *callback) { if (!d->Store3EventCallbacks.HasItem(callback)) { d->Store3EventCallbacks.Add(callback); } return true; } bool ScribeWnd::RemoveStore3EventHandler(LDataEventsI *callback) { d->Store3EventCallbacks.Delete(callback); return true; } bool ScribeWnd::OnMailTransferEvent(MailTransferEvent *t) { if (!Lock(_FL)) return false; LAssert(t); d->Transfers.Add(t); Unlock(); return true; } bool ScribeWnd::OnTransfer() { LVariant v; LArray Local; // Lock the transfer list if (Lock(_FL)) { // Take out a bunch of emails... for (int i=0; i<5 && d->Transfers.Length() > 0; i++) { auto t = d->Transfers[0]; LAssert(t); d->Transfers.DeleteAt(0, true); // Save them to a local array Local.Add(t); } Unlock(); if (Local.Length() == 0) // Didn't get any return false; } LArray Trans; for (auto &f: Folders) { if (f.Store) Trans.New() = f.Store->StartTransaction(); } for (auto Transfer: Local) { ReceiveStatus NewStatus = MailReceivedError; if (!Transfer) { LAssert(0); continue; } // We have to set Transfer->Status to something other than "waiting" // in all branches of this loop. Otherwise the account thread will hang. Accountlet *Acc = Transfer->Account; #if DEBUG_NEW_MAIL LgiTrace( "%s:%i - NewMail.OnTransfer t=%p receive=%i act=%i\n", _FL, Transfer, Acc->IsReceive(), Transfer->Action); #endif if (Acc->IsReceive()) { switch (Transfer->Action) { default: break; case MailDownload: case MailDownloadAndDelete: { // Import newly received mail from file ReceiveAccountlet *Receive = dynamic_cast(Acc); if (Receive) { LVariant Path = Receive->DestinationFolder(); ScribeFolder *Inbox = Path.Str() ? GetFolder(Path.Str()) : NULL; if (!Inbox) Inbox = GetFolder(FOLDER_INBOX); if (Inbox) { Mail *m = new Mail(this, Inbox->GetObject()->GetStore()->Create(MAGIC_MAIL)); if (m) { // Just in case m->SetParentFolder(Inbox); // Set the new flag... m->SetFlags(m->GetFlags() | MAIL_NEW, true, false); // Set the account to, which is used to map the email back // to the incoming account in the case that the headers // don't have the correct "To" information. m->SetAccountId(Receive->Id()); // Decode the email m->OnAfterReceive(Transfer->Rfc822Msg); LVariant v; m->SetServerUid(v = Transfer->Uid); // Save to permanent storage if (m->Save(Inbox)) { m->SetDirty(false); NewStatus = MailReceivedOk; // Only after we've safely stored the email can we // actually mark it as downloaded. if (Transfer->Uid) { Receive->AddMsg(Transfer->Uid); } } else LgiTrace("%s:%i - Error: Couldn't save mail to folders.\n", _FL); } else LgiTrace("%s:%i - Error: Memory alloc failed.\n", _FL); } else LgiTrace("%s:%i - Error: No Inbox.\n", _FL); } else LgiTrace("%s:%i - Error: Bad ptr.\n", _FL); break; } case MailHeaders: { LList *Lst; if (Transfer->Msg && (Lst = Transfer->GetList())) { //LgiTrace("Using Lst=%p\n", Lst); Lst->Insert(Transfer->Msg); NewStatus = MailReceivedOk; } break; } } } else if (Transfer->Send) { ScribeFolder *Outbox = GetFolder(Transfer->Send->SourceFolder); if (!Outbox) Outbox = GetFolder(FOLDER_OUTBOX); if (!Outbox) break; Outbox->GetMessageById(Transfer->Send->MsgId, [this, Transfer](auto m) { if (!m) { LAssert(!"Where is the email?"); LgiTrace("%s:%i - Can't find outbox for msg id '%s'\n", _FL, Transfer->Send->MsgId.Get()); } else { if (Transfer->OutgoingHeaders) { m->SetInternetHeader(Transfer->OutgoingHeaders); DeleteArray(Transfer->OutgoingHeaders); } m->OnAfterSend(); m->Save(); // Do filtering LVariant DisableFilters; GetOptions()->GetValue(OPT_DisableUserFilters, DisableFilters); if (!DisableFilters.CastInt32()) { // Run the filters List Filters; GetFilters(Filters, false, true, false); if (Filters[0]) { List In; In.Insert(m); Filter::ApplyFilters(0, Filters, In); } } // Add to bayesian spam whitelist... LVariant v; ScribeBayesianFilterMode FilterMode = BayesOff; GetOptions()->GetValue(OPT_BayesFilterMode, v); FilterMode = (ScribeBayesianFilterMode) v.CastInt32(); if (FilterMode != BayesOff && m->GetObject()) { - GDataIt To = m->GetObject()->GetList(FIELD_TO); + LDataIt To = m->GetObject()->GetList(FIELD_TO); if (To) { for (LDataPropI *a = To->First(); a; a = To->Next()) { if (a->GetStr(FIELD_EMAIL)) WhiteListIncrement(a->GetStr(FIELD_EMAIL)); } } } // FIXME: // NewStatus = MailReceivedOk; } }); } #if DEBUG_NEW_MAIL LgiTrace( "%s:%i - NewMail.OnTransfer t=%p NewStatus=%i\n", _FL, Transfer, NewStatus); #endif if (NewStatus != MailReceivedOk) { // So tell the thread not to delete it from the server LgiTrace("%s:%i - Mail[%i] error: %s\n", _FL, Transfer->Index, ReceiveStatusName(Transfer->Status)); Transfer->Action = MailNoop; } Transfer->Status = NewStatus; } return Local.Length() > 0; } bool ScribeWnd::OnIdle() { bool Status = false; for (auto a : Accounts) Status |= a->Receive.OnIdle(); Status |= OnTransfer(); LMessage m(M_SCRIBE_IDLE); BayesianFilter::OnEvent(&m); SaveDirtyObjects(); #ifdef _DEBUG static uint64_t LastTs = 0; auto Now = LCurrentTime(); if (Now - LastTs >= 1000) { LastTs = Now; if (Thing::DirtyThings.Length() > 0) LgiTrace("%s:%i - Thing::DirtyThings=" LPrintfInt64 "\n", _FL, Thing::DirtyThings.Length()); } #endif return Status; } void ScribeWnd::OnScriptCompileError(const char *Source, Filter *f) { const char *CompileMsg = LLoadString(IDS_ERROR_SCRIPT_COMPILE); LArray Actions; Actions.Add(LLoadString(IDS_SHOW_CONSOLE)); Actions.Add(LLoadString(IDS_OPEN_SOURCE)); Actions.Add(LLoadString(IDS_OK)); if (!d->Bar) { // FYI Capabilities are handled in ScribeWnd::StartAction. d->ErrSource = Source; d->ErrFilter = f; d->Bar = new MissingCapsBar(this, &d->MissingCaps, CompileMsg, this, Actions); AddView(d->Bar, 2); AttachChildren(); OnPosChange(); } } diff --git a/Code/ScribeAttachment.cpp b/Code/ScribeAttachment.cpp --- a/Code/ScribeAttachment.cpp +++ b/Code/ScribeAttachment.cpp @@ -1,1384 +1,1383 @@ /* ** FILE: ScribeAttachment.cpp ** AUTHOR: Matthew Allen ** DATE: 7/12/98 ** DESCRIPTION: Scribe Attachments ** ** Copyright (C) 1998, Matthew Allen ** fret@memecode.com */ #include #include #include #include #include "Scribe.h" #include "resdefs.h" #include "lgi/common/NetTools.h" #include "lgi/common/ProgressDlg.h" #include "lgi/common/Tnef.h" #include "lgi/common/LgiRes.h" #include "lgi/common/TextConvert.h" #include "lgi/common/FileSelect.h" extern char *ExtractCodePage(char *ContentType); #ifdef WIN32 char NotAllowed[] = "\\/*:?\"|<>"; #else char NotAllowed[] = "\\/"; #endif ////////////////////////////////////////////////////////////////////////////// char *StripPath(const char *Full) { if (Full) { auto Dos = strrchr(Full, '\\'); auto Unix = strrchr(Full, '/'); if (Dos) { return NewStr(Dos+1); } else if (Unix) { return NewStr(Unix+1); } } return NewStr(Full); } void CleanFileName(char *i) { if (i) { char *o = i; while (*i) { if ((uint8_t)*i >= ' ' && !strchr(NotAllowed, *i)) { *o++ = *i; } i++; } *o++ = 0; } } ////////////////////////////////////////////////////////////////////////////// void Attachment::_New(LDataI *object) { DefaultObject(object); LAssert(GetObject() != NULL); Owner = 0; Msg = 0; IsResizing = false; } Attachment::Attachment(ScribeWnd *App, Attachment *From) : Thing(App) { _New(From && From->GetObject() ? From->GetObject()->GetStore()->Create(MAGIC_ATTACHMENT) : 0); if (From) { char *Ptr = 0; ssize_t Len = 0; if (From->Get(&Ptr, &Len)) { Set(Ptr, Len); SetName(From->GetName()); SetMimeType(From->GetMimeType()); SetContentId(From->GetContentId()); SetCharset(From->GetCharset()); } } } Attachment::Attachment(ScribeWnd *App, LDataI *object, const char *Import) : Thing(App) { _New(object); if (Import) ImportFile(Import); } Attachment::~Attachment() { DeleteObj(Msg); if (Owner) Owner->Attachments.Delete(this); } bool Attachment::ImportFile(const char *FileName) { LAutoPtr f(new LFile); if (!f) { LAssert(!"Out of memory"); return false; } LString Mime = ScribeGetFileMimeType(FileName); if (!f->Open(FileName, O_READ)) { LAssert(!"Can't open file."); return false; } char *c = strrchr((char*)FileName, DIR_CHAR); if (c) SetName(c + 1); else SetName(FileName); if (Mime) SetMimeType(Mime); LAutoStreamI s(f.Release()); GetObject()->SetStream(s); return true; } bool Attachment::ImportStream(const char *FileName, const char *MimeType, LAutoStreamI Stream) { if (!FileName || !MimeType || !Stream) { LAssert(!"Parameter error"); return false; } char *c = strrchr((char*)FileName, DIR_CHAR); if (c) SetName(c + 1); else SetName(FileName); SetMimeType(MimeType); GetObject()->SetStream(Stream); return true; } void Attachment::SetOwner(Mail *msg) { Owner = msg; } bool Attachment::CallMethod(const char *MethodName, LVariant *Ret, LArray &Args) { ScribeDomType Fld = StrToDom(MethodName); *Ret = false; switch (Fld) { case SdSave: // Type: (String FileName) { auto Fn = Args.Length() > 0 ? Args[0]->Str() : NULL; if (Fn) *Ret = SaveTo(Fn, true); return true; } default: break; } return Thing::CallMethod(MethodName, Ret, Args); } bool Attachment::GetVariant(const char *Name, LVariant &Value, const char *Array) { ScribeDomType Fld = StrToDom(Name); switch (Fld) { case SdLength: // Type: Int64 { Value = GetSize(); break; } case SdName: // Type: String { Value = GetName(); break; } case SdMimeType: // Type: String { Value = GetMimeType(); break; } case SdContentId: // Type: String { Value = GetContentId(); break; } case SdData: // Type: Binary { LAutoPtr s(GotoObject(_FL)); if (!s) return false; Value.Empty(); Value.Type = GV_BINARY; if ((Value.Value.Binary.Data = new char[Value.Value.Binary.Length = (int)s->GetSize()])) s->Read(Value.Value.Binary.Data, Value.Value.Binary.Length); break; } case SdType: // Type: Int32 { Value = GetObject()->Type(); break; } default: { return false; } } return true; } Thing &Attachment::operator =(Thing &t) { LAssert(0); return *this; } void IncFileIndex(char *FilePath, size_t FilePathLen) { char File[MAX_PATH_LEN]; char Ext[256] = ""; // Get the extension part char *Dot = strrchr(FilePath, '.'); if (Dot) { strcpy_s(Ext, sizeof(Ext), Dot); } // Get the filename part ssize_t FileLen = (ssize_t)strlen(FilePath) - strlen(Ext); memcpy(File, FilePath, FileLen); File[FileLen] = 0; // Seek to start of digits char *Digits = File + strlen(File) - 1; while (IsDigit(*Digits) && Digits > File) { Digits--; } if (!IsDigit(*Digits)) Digits++; // Increment the index int Index = atoi(Digits); sprintf_s(Digits, sizeof(File)-(Digits-File), "%i", Index + 1); // Write the resulting filename sprintf_s(FilePath, FilePathLen, "%s%s", File, Ext); } void Attachment::SetMsg(Mail *m) { Msg = m; } Mail *Attachment::GetMsg() { if (!Msg && IsMailMessage()) { // is an email LAutoStreamI f = GetObject()->GetStream(_FL); if (f) { if ((Msg = new Mail(App))) { Msg->SetWillDirty(false); Msg->App = App; Msg->ParentFile = this; Msg->OnAfterReceive(f); } } } return Msg; } bool Attachment::GetIsResizing() { return IsResizing; } void Attachment::SetIsResizing(bool b) { IsResizing = b; Update(); } bool Attachment::IsMailMessage() { return GetMimeType() && !_stricmp(GetMimeType(), sMimeMessage); } bool Attachment::IsVCalendar() { return GetMimeType() && ( !_stricmp(GetMimeType(), sMimeVCalendar) || !_stricmp(GetMimeType(), sMimeICalendar) ); } bool Attachment::IsVCard() { return GetMimeType() && !_stricmp(GetMimeType(), sMimeVCard); } char *Attachment::GetDropFileName() { if (!DropFileName) DropFileName = MakeFileName(); return DropFileName; } bool Attachment::GetDropFiles(LString::Array &Files) { bool Status = false; if (GetDropFileName()) { char p[MAX_PATH_LEN]; LMakePath(p, sizeof(p), ScribeTempPath(), DropFileName); if (SaveTo(p, true)) { Files.Add(p); Status = true; } } return Status; } LAutoString Attachment::MakeFileName() { auto Name = GetName(); LAutoString CleanName; if (Name) { CleanName.Reset(StripPath(Name)); CleanFileName(CleanName); } else { LArray Ext; char s[256] = "Attachment"; auto MimeType = GetMimeType(); LGetMimeTypeExtensions(MimeType, Ext); if (Ext.Length()) { size_t len = strlen(s); sprintf_s(s+len, sizeof(s)-len, ".%s", Ext[0].Get()); } CleanName.Reset(NewStr(s)); } return CleanName; } void Attachment::OnOpen(LView *Parent, char *Dest) { bool VCal; if (GetMsg()) { // Open the mail message... Msg->DoUI(Owner); } else if ((VCal = IsVCalendar()) || IsVCard()) { // Open the event or contact... Thing *c = App->CreateItem(VCal ? MAGIC_CALENDAR : MAGIC_CONTACT, 0, false); if (c) { LAutoPtr f(GotoObject(_FL)); if (f) { if (c->Import(f, GetMimeType())) { c->DoUI(); } else LgiMsg(Parent, "Failed to parse calendar.", "Error"); } else LgiTrace("%s:%i - Failed to get attachment stream.\n", _FL); } } else // is some generic file { bool IsExe = false; int TnefSizeLimit = 8 << 20; LStream *TnefStream = 0; LArray TnefIndex; int64 AttachPos = -1; LAutoStreamI f = GetObject()->GetStream(_FL); if (f) { IsExe = LIsFileExecutable(GetName(), f, AttachPos = f->GetPos(), f->GetSize()); if (!IsExe && f->GetSize() < TnefSizeLimit) { f->SetPos(AttachPos); if (!TnefReadIndex(f, TnefIndex)) { DeleteObj(TnefStream); } } f.Reset(); } if (IsExe) { LgiMsg(Parent, LLoadString(IDS_ERROR_EXE_FILE), AppName); return; } // Check for TNEF if (TnefStream) { LStringPipe p; for (unsigned n=0; nSize); p.Print("\t%s (%s)\n", TnefIndex[n]->Name, Size); } char *FileList = p.NewStr(); if (Owner && LgiMsg(Parent, LLoadString(IDS_ASK_TNEF_DECODE), AppName, MB_YESNO, FileList) == IDYES) { char *Tmp = ScribeTempPath(); if (Tmp) { for (unsigned i=0; iName); LFile Out; if (Out.Open(s, O_WRITE)) { Out.SetSize(0); if (TnefExtract(TnefStream, &Out, TnefIndex[i])) { Out.Close(); Attachment *NewFile = 0; Owner->AttachFile(NewFile = new Attachment(App, GetObject()->GetStore()->Create(MAGIC_ATTACHMENT), s)); if (NewFile && Owner->GetUI()) { AttachmentList *Lst = Owner->GetUI()->GetAttachments(); if (Lst) { Lst->Insert(NewFile); Lst->ResizeColumnsToContent(); } } } Out.Close(); } } } Owner->Save(); OnDeleteAttachment(Parent, false); } DeleteObj(TnefStream); DeleteArray(FileList); } else { // Open file... LAutoString FileToExecute; char *Tmp = ScribeTempPath(); if (!Tmp) return; // get the file name char FileName[MAX_PATH_LEN]; LAutoString CleanName = MakeFileName(); if (CleanName) { LMakePath(FileName, sizeof(FileName), Tmp, CleanName); while (LFileExists(FileName)) { IncFileIndex(FileName, sizeof(FileName)); } // write the file out if (SaveTo(FileName)) { FileToExecute.Reset(NewStr(FileName)); } } // open the file auto Mime = GetMimeType(); if (FileToExecute) { LAutoString AssociatedApp; LXmlTag *FileTypes = App->GetOptions()->LockTag(OPT_FileTypes, _FL); if (FileTypes) { auto Mime = GetMimeType(); auto FileName = GetName(); for (auto t: FileTypes->Children) { char *mt = t->GetAttr("mime"); char *ext = t->GetAttr("extension"); bool MimeMatch = mt && Mime && !_stricmp(mt, Mime); bool ExtMatch = ext && FileName && MatchStr(ext, FileName); if (MimeMatch || ExtMatch) { AssociatedApp.Reset(TrimStr(t->GetContent())); break; } } App->GetOptions()->Unlock(); } if (AssociatedApp) { const char *s = AssociatedApp; LAutoString Exe(LTokStr(s)); if (Exe) { char Args[MAX_PATH_LEN+100]; if (!strchr(s, '%') || sprintf_s(Args, sizeof(Args), s, FileToExecute.Get()) < 0) { if (sprintf_s(Args, sizeof(Args), "\"%s\"", FileToExecute.Get()) < 0) Args[0] = 0; } if (Args[0] && LExecute(Exe, Args)) { // Successful.. return; } } } if (!LExecute(FileToExecute, 0, Tmp)) { // if the default open fails.. open as text LString AppPath = LGetAppForMimeType(Mime ? Mime : sTextPlain); bool Status = false; if (AppPath) { char *s = strchr(AppPath, '%'); if (s) s[0] = 0; Status = LExecute(AppPath, FileToExecute, Tmp); } if (!Status) { LgiMsg(Parent, "Couldn't open file.", AppName, MB_OK); } } } } } } void Attachment::OnDeleteAttachment(LView *Parent, bool Ask) { if (Owner) { LVariant ConfirmDelete; App->GetOptions()->GetValue(OPT_ConfirmDelete, ConfirmDelete); if (!Ask || !ConfirmDelete.CastInt32() || LgiMsg(GetList(), LLoadString(IDS_DELETE_ASK), AppName, MB_YESNO) == IDYES) { List Sel; LList *p = GetList(); if (p) { p->GetSelection(Sel); } else { Sel.Insert(this); } for (LListItem *i: Sel) { Attachment *a = dynamic_cast(i); if (a) { if (p) { p->Remove(i); } a->Owner->DeleteAttachment(a); } } if (p) { p->Invalidate(); } } } } bool Attachment::SaveTo(char *FileName, bool Quite, LView *Parent) { bool Status = false; if (FileName) { LFile Out; Status = true; if (LFileExists(FileName)) { if (Quite) { return true; } else { Out.Close(); LString Msg = AskOverwriteMsg(FileName); Status = LgiMsg(Parent ? Parent : App, Msg, AppName, MB_YESNO) == IDYES; } } if (!Out.Open(FileName, O_WRITE)) { if (!Quite) LgiMsg(App, LLoadString(IDS_ERROR_CANT_WRITE), AppName, MB_OK, FileName); else LgiTrace("%s:%i - Can't open '%s' for writing (err=0x%x)\n", _FL, FileName, Out.GetError()); Status = false; } if (Status) { Out.SetSize(0); if (GetObject()) { int BufSize = 64 << 10; char *Buf = new char[BufSize]; LStreamI *f = GotoObject(_FL); if (f && Buf) { f->SetPos(0); Out.SetSize(0); int64 MySize = f->GetSize(); int64 s = MySize; while (s > 0) { ssize_t r = (int)MIN(BufSize, s); r = f->Read(Buf, r); if (r > 0) { Out.Write(Buf, r); s -= r; } else break; } int64 OutPos = Out.GetPos(); if (OutPos < MySize) { // Error writing to disk... Out.Close(); FileDev->Delete(FileName, false); LAssert(!"Failed to write whole attachment to disk."); Status = false; } } else { LgiTrace("%s:%i - GotoObject failed.\n", _FL); Status = false; } DeleteObj(f); DeleteArray(Buf); } /* else if (Data) { int Written = Out.Write(Data, Size); Status = Written == Size; } */ } } return Status; } void Attachment::OnSaveAs(LView *Parent) { auto Name = GetName(); char *n = StripPath(Name); if (!n) { n = NewStr("untitled"); } if (n) { CleanFileName(n); auto Select = new LFileSelect(Parent); Select->Type("All files", LGI_ALL_FILES); Select->Name(n); List Files; if (LListItem::Parent) { LListItem::Parent->GetSelection(Files); } else { Files.Insert(this); } if (Files.Length() > 0) { auto DoSave = [&](LFileSelect *Select) { char Dir[MAX_PATH_LEN]; strcpy_s(Dir, sizeof(Dir), Select->Name()); if (Files.Length() > 1) { // Loop through all the files and write them to that directory for (LListItem *i: Files) { Attachment *a = dynamic_cast(i); if (a) { char Path[MAX_PATH_LEN]; auto d = StripPath(a->GetName()); if (d) { sprintf_s(Path, sizeof(Path), "%s%s%s", Dir, DIR_STR, d); a->SaveTo(Path); DeleteArray(d); } } } } else { // Write the file Attachment *a = dynamic_cast(Files[0]); if (a) { a->SaveTo(Dir, false, Parent); } } }; if (Files.Length() > 1) { // multiple files, ask which directory to write to Select->OpenFolder([&](auto dlg, auto status) { if (status) DoSave(dlg); delete dlg; }); } else { // single file, ask for filename and path Select->Save([&](auto dlg, auto status) { if (status) DoSave(dlg); delete dlg; }); } } DeleteArray(n); } } bool Attachment::OnKey(LKey &k) { if (k.vkey == LK_RETURN && k.IsChar) { if (k.Down()) { OnOpen(GetList()); } return true; } return false; } void Attachment::OnMouseClick(LMouse &m) { auto mt = GetMimeType(); bool OpenAttachment = false; if (m.IsContextMenu()) { // open the right click menu LSubMenu RClick; LString MimeType = GetMimeType(); RClick.AppendItem(LLoadString(IDS_ADD_TO_CAL), IDM_ADD_TO_CAL, IsVCalendar()); RClick.AppendSeparator(); RClick.AppendItem(LLoadString(IDS_OPEN), IDM_OPEN, true); RClick.AppendItem(LLoadString(IDS_SAVEAS), IDM_SAVEAS, true); RClick.AppendItem(LLoadString(IDS_DELETE), IDM_DELETE, true); RClick.AppendSeparator(); RClick.AppendItem(LLoadString(IDS_RESIZE), IDM_RESIZE, MimeType.Lower().Find("image/") >= 0); if (Parent->GetMouse(m, true)) { switch (RClick.Float(Parent, m.x, m.y)) { case IDM_OPEN: { OpenAttachment = true; break; } case IDM_DELETE: { OnDeleteAttachment(Parent, true); break; } case IDM_SAVEAS: { OnSaveAs(Parent); break; } case IDM_ADD_TO_CAL: { ScribeFolder *Cal = App->GetFolder(FOLDER_CALENDAR); if (!Cal) LgiMsg(Parent, "Can't find the calendar folder.", AppName); else { Thing *c = App->CreateItem(MAGIC_CALENDAR, 0, false); if (c) { LAutoPtr f(GotoObject(_FL)); if (f) { if (c->Import(c->AutoCast(f), mt)) { c->Save(Cal); c->DoUI(); } else LgiTrace("%s:%i - Failed to import cal stream.\n", _FL); } else LgiTrace("%s:%i - Failed to get attachment stream.\n", _FL); } else LgiTrace("%s:%i - Failed to create calendar obj.\n", _FL); } break; } case IDM_RESIZE: { List Sel; if (GetList() && GetList()->GetSelection(Sel)) { for (auto a: Sel) { if (!a->Owner) { LgiTrace("%s:%i - No owner?", _FL); break; } a->Owner->ResizeImage(a); } GetList()->ResizeColumnsToContent(); } break; } } } } else if (m.Double()) { OpenAttachment = true; } if (OpenAttachment) { // open the attachment OnOpen(Parent); } } bool Attachment::GetFormats(LDragFormats &Formats) { Formats.SupportsFileDrops(); return Formats.Length() > 0; } bool Attachment::GetData(LArray &Data) { int SetCount = 0; for (unsigned idx=0; idx Att; if (Parent->GetSelection(Att)) { LString::Array Files; for (auto a: Att) { // char *Nm = a->GetName(); if (!a->DropSourceFile || !LFileExists(a->DropSourceFile)) { a->DropSourceFile.Reset(); char p[MAX_PATH_LEN]; LAutoString Clean = a->MakeFileName(); LMakePath(p, sizeof(p), ScribeTempPath(), Clean); char Ext[256]; char *d = strrchr(p, '.'); if (!d) d = p + strlen(p); strcpy_s(Ext, sizeof(Ext), d); for (int i=1; LFileExists(p); i++) { sprintf_s(d, sizeof(p)-(d-p), "_%i%s", i, Ext); } if (a->SaveTo(p, true)) { a->DropSourceFile.Reset(NewStr(p)); } } if (a->DropSourceFile) { Files.Add(a->DropSourceFile.Get()); } else LAssert(0); } if (Files.First()) { LMouse m; App->GetMouse(m, true); if (CreateFileDrop(&dd, m, Files)) { SetCount++; } } } } } return SetCount > 0; } LStreamI *Attachment::GotoObject(const char *file, int line) { if (!GetObject()) return 0; LAutoStreamI s = GetObject()->GetStream(file, line); return s.Release(); } int Attachment::Sizeof() { return 0; } bool Attachment::Serialize(LFile &f, bool Write) { /* ulong Magic = MAGIC_ATTACHMENT; LView *Parent = Window; if (Owner && Owner->GetUi()) { Parent = Owner->GetUi(); } if (Write) { f << Magic; f << Content; // Check we have the data to write out, as it will effect the // size we write out before the data bool DataOk = false; LFile In; if (Data) { DataOk = true; } else if (ImportName) { // Check we can open the file... while (!In.Open(ImportName, O_READ)) { char Msg[256]; sprintf_s(Msg, sizeof(Msg), LLoadString(IDS_ERROR_CANT_READ), ImportName); LAlert Dlg( Parent, AppName, Msg, LLoadString(IDS_RETRY), LLoadString(IDS_CANCEL)); int Result = Dlg.DoModal(); if (Result == 2) { break; } } DataOk = In.IsOpen(); } else { // We're skipping the data already on disk DataOk = true; } if (!DataOk) { // No point continuing return false; } f << Size; WriteStr(f, Name); if (Data) { // write the file itself f.Write(Data, Size); } else if (ImportName) { // import from the file uint64 Last = LCurrentTime(); LProgressDlg *Prog = 0; int BufSize = 64 << 10; uchar *Buf = new uchar[BufSize]; if (Buf) { int s = Size; while (s > 0 && f.GetStatus()) { int r = min(s, BufSize); r = In.Read(Buf, r); f.Write(Buf, r); s -= r; uint64 Now = LCurrentTime(); if (Prog) { if (Now - Last > 300) { Prog->Value((Size-s) >> 10); LYield(); Last = Now; if (Prog->Cancel()) { DeleteArray(Buf); DeleteObj(Prog); return false; } } } else if (Now - Last > 1000) { Prog = new LProgressDlg(Parent); if (Prog) { Prog->SetDescription("Importing file..."); Prog->SetLimits(0, Size >> 10); Prog->SetType("K"); } Last = Now; } } DeleteArray(ImportName); DeleteObj(Prog); } DeleteArray(Buf); } else { // skip over data on hard disk f.Seek(Size, SEEK_CUR); } // new style fields if (MimeType) { WriteStrField(FIELD_MIME_TYPE, MimeType); } if (ContentId) { WriteStrField(FIELD_CONTENT_ID, ContentId); } } else { f >> Magic; if (Magic == MAGIC_ATTACHMENT) // The versions before v1.25 didn't // set this correctly, but that is so old // now, I've removed the hack to allow // the attachment in. { f >> Content; f >> Size; DeleteArray(Name); Name = ReadStr(f PassDebugArgs); // Skip over the data DeleteArray(Data); int StartPos = f.GetPos(); f.Seek(Size, SEEK_CUR); int ExtraPos = f.GetPos(); // read list of new-style fields bool Done = false; bool Eob = false; while ( !(Eob = Store->EndOfObj(f)) && !Done) { short FieldId = 0; ulong FieldSize = 0; f >> FieldId; switch (FieldId) { ReadStrField(FIELD_MIME_TYPE, MimeType); ReadStrField(FIELD_CONTENT_ID, ContentId); default: { // Error: unknown chunk SetDirty(); Done = true; break; } } } LFormatSize(SizeStr, Size); } else return false; } return f.GetStatus(); */ return false; } char *GetSubField(char *s, char *Field, bool AllowConversion = true) { char *Status = 0; if (s && Field) { s = strchr(s, ';'); if (s) { s++; size_t FieldLen = strlen(Field); char White[] = " \t\r\n"; while (*s) { // Skip leading whitespace while (*s && (strchr(White, *s) || *s == ';')) s++; // Parse field name if (IsAlpha(*s)) { char *f = s; while (*s && (IsAlpha(*s) || *s == '-')) s++; bool HasField = ((s-f) == FieldLen) && (_strnicmp(Field, f, FieldLen) == 0); while (*s && strchr(White, *s)) s++; if (*s == '=') { s++; while (*s && strchr(White, *s)) s++; if (*s && strchr("\'\"", *s)) { // Quote Delimited Field char d = *s++; char *e = strchr(s, d); if (e) { if (HasField) { if (AllowConversion) { Status = DecodeRfc2047(NewStr(s, e-s)); } else { Status = NewStr(s, e-s); } break; } s = e + 1; } else break; } else { // Delimited Field char *e = s; while (*e && *e != ';') e++; if (HasField) { Status = DecodeRfc2047(NewStr(s, e-s)); break; } s = e; } } else break; } else break; } } } return Status; } const char *Attachment::GetText(int i) { if (FieldArray.Length()) { return "This is an attachment!!!!!"; } else { switch (i) { case 0: { auto Nm = GetName(); if (IsResizing) { Buf.Printf("%s (%s)", Nm, LLoadString(IDS_RESIZING)); return Buf; } return Nm; } case 1: { static char s[64]; LFormatSize(s, sizeof(s), GetSize()); return s; } case 2: { return GetMimeType(); } case 3: { return GetContentId(); } } } return 0; } bool Attachment::Get(char **ptr, ssize_t *size) { - if (!ptr || !size || *size <= 0) + if (!ptr || !size) return false; LStreamI *f = GotoObject(_FL); if (!f) return false; *size = f->GetSize(); *ptr = new char[*size+1]; if (*ptr) { auto r = f->Read(*ptr, *size); (*ptr)[r] = 0; - } DeleteObj(f); return true; } bool Attachment::Set(LAutoStreamI Stream) { if (!GetObject()) { LAssert(0); return false; } if (!GetObject()->SetStream(Stream)) { LAssert(0); return false; } return true; } bool Attachment::Set(char *ptr, ssize_t size) { LAutoStreamI s(new LMemStream(ptr, size)); return Set(s); } diff --git a/Code/ScribeFinder.cpp b/Code/ScribeFinder.cpp --- a/Code/ScribeFinder.cpp +++ b/Code/ScribeFinder.cpp @@ -1,1406 +1,1406 @@ /* ** FILE: ScribeFinder.cpp ** AUTHOR: Matthew Allen ** DATE: 3/6/1999 ** DESCRIPTION: Scribe finder tool ** ** Copyright (C) 1999, Matthew Allen ** fret@memecode.com */ #include #include #include #include #include "Scribe.h" #include "lgi/common/Edit.h" #include "lgi/common/Button.h" #include "lgi/common/CheckBox.h" #include "lgi/common/Combo.h" #include "../Resources/resdefs.h" #include "lgi/common/DragAndDrop.h" #include "lgi/common/TableLayout.h" #include "lgi/common/LgiRes.h" #define IDC_RESULTS 90 ////////////////////////////////////////////////////////////////////////////// int FindCompare(LListItem *a, LListItem *b, NativeInt Data); ////////////////////////////////////////////////////////////////////////////////// #include "LMarkColourSelect.h" LMarkColourSelect::LMarkColourSelect() : ResObject(Res_Custom) { } LArray LMarkColourSelect::GetSelected() { LArray a; for (int i=0; iX() + AnyTxt->X() + (IDM_MARK_MAX * (ColPx + Pad)) + (Pad * 3) + 2; Inf.Width.Min = None->X() + AnyTxt->X() + IDM_MARK_MAX + (Pad * 4) + 2; } else { Inf.Width.Max = Inf.Width.Min = -1; } } return true; } void LMarkColourSelect::OnPressColour(size_t i) { ColSel[i] = !ColSel[i]; if (!ColSel[i]) Any = false; Invalidate(); SendNotify(LNotifyValueChanged); } void LMarkColourSelect::SelectNone() { Any = false; memset(ColSel, 0, sizeof(ColSel)); Invalidate(); SendNotify(LNotifyValueChanged); } void LMarkColourSelect::SelectAll() { Any = true; for (int i=0; iColour(low); pDC->Box(&cli); cli.Inset(1, 1); pDC->Colour(bk); pDC->Rectangle(&cli); int x = Pad; if (None) { None->GetFont()->Colour(txt, bk); NoneRc.ZOff(None->X()-1, None->Y()-1); NoneRc.Offset(cli.x1+x, cli.y1+Pad); None->Draw(pDC, NoneRc.x1, NoneRc.y1); NoneRc.Inset(-Pad, -Pad); x += None->X() + Pad; } for (int i=0; iColour(MarkColours32[i], 32); if (ColSel[i]) pDC->Rectangle(&r); else { LRect t = r; pDC->Box(&t); t.Inset(1, 1); pDC->Box(&t); } x += r.X() + Pad; } if (AnyTxt) { AnyTxt->GetFont()->Colour(txt, bk); AnyRc.ZOff(AnyTxt->X()-1, AnyTxt->Y()-1); AnyRc.Offset(cli.x1+x, cli.y1+Pad); AnyTxt->Draw(pDC, cli.x1 + x, cli.x1 + Pad); AnyRc.Inset(-Pad, -Pad); x += AnyTxt->X() + Pad; } } class LMarkColourSelectFactory : public LViewFactory { LView *NewView(const char *Class, LRect *Pos, const char *Text) { if (!stricmp(Class, "LMarkColourSelect")) return new LMarkColourSelect(); return NULL; } } MarkColourSelectFactory; ////////////////////////////////////////////////////////////////////////////// class ResultItem : public LListItem { friend int FindCompare(LListItem *a, LListItem *b, NativeInt Data); friend class ResultList; LString Path; Thing *T; bool GetData(LArray &Data) { bool Status = false; List Objs; LList *ParentList = LListItem::Parent; if (ParentList && ParentList->GetSelection(Objs)) { for (unsigned di=0; di(i); if (Ri && Ri->T) Fmt->ThingAt(n++, Ri->T); } Status = dd.Data[0].SetBinary(Fmt->Sizeof(), Fmt); free(Fmt); } } else if (!_stricmp(LGI_FileDropFormat, dd.Format)) { LMouse m; GetList()->GetMouse(m, true); LDragDropSource *Src = dynamic_cast(GetList()); if (!Src) return false; LString::Array Files; for (auto i: Objs) { ResultItem *Ri = dynamic_cast(i); if (Ri) { Status |= Ri->T->GetDropFiles(Files); } } if (Status) { Status = Src->CreateFileDrop(&dd, m, Files); } } } } return Status; } int Type() { return T ? T->Type() : 0; } int Sizeof() { LAssert(0); return 0; } bool Serialize(LFile &f, bool Write) { LAssert(0); return 0; } public: ResultItem(ScribeWnd *App, Thing *t) { T = t; LAssert(T != NULL); auto m = t->IsMail(); if (m) { auto colour = m->GetMarkColour(); if (colour > 0) { LColour c((uint32_t)colour, 32); GetCss(true)->BackgroundColor(c.Mix(L_WORKSPACE, Mail::MarkColourMix)); } } } Thing *GetThing() { return T; } const char *GetText(int i = 0) { const char *Status = 0; int *DefFlds = T->GetDefaultFields(); if (i < 4) { Status = DefFlds ? T->GetFieldText(DefFlds[i]) : (char*)""; } else if (i == 4) { ScribeFolder *f = T->GetFolder(); if (f) { Path = f->GetPath(); } Status = Path; } return Status; } int GetImage(int Flags = 0) { return T->GetImage(Flags); } LView *DoUI() { return T->DoUI(); } }; class ResultList : public LList, public LDragDropSource { friend int FindCompare(LListItem *a, LListItem *b, NativeInt Data); int Col; bool Ascend; public: ResultList(int id, int x, int y, int cx, int cy, const char *name = "") : LList(id, x, y, cx, cy, name) { Col = -1; Ascend = true; MultiSelect(true); } void OnItemBeginDrag(LListItem *Item, LMouse &m) { Drag(this, m.Event, DROPEFFECT_MOVE); } bool GetFormats(LDragFormats &Formats) { Formats.Supports(ScribeThingList); Formats.SupportsFileDrops(); return true; } bool GetData(LArray &Data) { if (!Data.Length()) return false; List Sel; if (!GetSelection(Sel)) return false; return Sel[0]->GetData(Data); } void OnItemClick(LListItem *Item, LMouse &m) { ResultItem *i = dynamic_cast(Item); if (i) { if (m.Down() && m.Double()) { i->DoUI(); } /* else if (i->T && m.Right()) { i->T->DoContextMenu(this); } */ } } void SetSort(int col, int ascend) { Col = col; Ascend = ascend != 0; Sort(FindCompare, (NativeInt) this); for (int i=0; iMark( (Col == i) ? (Ascend) ? GLI_MARK_DOWN_ARROW : GLI_MARK_UP_ARROW : GLI_MARK_NONE); } } } void OnColumnClick(int col, LMouse &m) { if (Col != col) { SetSort(col, true); } else { SetSort(Col, !Ascend); } } }; int FindCompare(LListItem *a, LListItem *b, NativeInt Data) { ResultList *List = (ResultList*) Data; ResultItem *t1 = dynamic_cast(a); ResultItem *t2 = dynamic_cast(b); if (List && t1 && t2) { // do specific compare on mail items.. Mail *m1 = t1->T->IsMail(); Mail *m2 = t2->T->IsMail(); int Mul = (List->Ascend) ? 1 : -1; if (m1 && m2) { switch (List->Col) { case 2: { // size return Mul * (int)(m1->TotalSizeof() - m2->TotalSizeof()); break; } case 3: { // date int Sort = 0; auto d1 = m1->GetDateSent(); auto d2 = m2->GetDateSent(); if (d1 && d2) { if (*d1 < *d2) Sort = -1; if (*d1 > *d2) Sort = 1; return Mul * Sort; } break; } } } // default back to a string compare const char *A = a->GetText(List->Col); const char *B = b->GetText(List->Col); if (A && B) { return Mul * _stricmp(A, B); } } return 0; } /////////////////////////////////////////////////////////////////////// #define M_END_SEARCH (M_USER+0x145) class FindThread : public LThread, public LMutex { // Work vars ScribeWnd *App; ScribeFolder *Folder; ResultList *Results; bool InMail; int MailF; bool InContacts; int ContactF; LArray SearchText; bool Deep; bool CaseSensitive; bool MatchWord; bool Loop; LArray Colours; LAutoPtr GroupMap; // Status uint64 StartTime; int ItemsSearched; LString CurFolder; // Thread vars LViewI *Notify; bool MatchString(const char *String, char *Pattern) { if (String && Pattern) { size_t Len = strlen(Pattern); auto s = String; do { auto c = (CaseSensitive)?strstr(s, Pattern):stristr(s, Pattern); if (c) { if (MatchWord) { bool Before = c <= String || IsWordBoundry(c[-1]); bool After = c[Len] == 0 || IsWordBoundry(c[Len]); if (Before && After) return true; } else { return true; } } else break; s = c + Len; } while (s && *s); } return false; } bool MatchAddress(LDataPropI *Addr, char *Pattern, int CC = -1) { bool Status = false; if (Addr && Pattern && (CC < 0 || Addr->GetInt(FIELD_CC) == CC)) { if (Addr->GetStr(FIELD_NAME)) { Status |= MatchString(Addr->GetStr(FIELD_NAME), Pattern); } if (Addr->GetStr(FIELD_EMAIL)) { Status |= MatchString(Addr->GetStr(FIELD_EMAIL), Pattern); } // Status |= (Addr->Name) ? stristr(Addr->Name, Text) != 0 : 0; // Status |= (Addr->Addr) ? stristr(Addr->Addr, Text) != 0 : 0; } return Status; } bool MatchMail(Mail *mail, int Field = -1) { if (!mail) return false; #define StrField(fld, id) if (id == Field || Field < 0) Status |= (MatchString(fld, Text) != 0); bool Result = true; for (unsigned i=0; iTotalSizeof(); int Value = (int) (atof(StartNumber) * Scale); if (_stricmp(Op, "=") == 0) { Status = Size == Value; } else if (_stricmp(Op, "!=") == 0) { Status = Size != Value; } else if (_stricmp(Op, "<") == 0) { Status = Size < Value; } else if (_stricmp(Op, ">") == 0) { Status = Size > Value; } else if (_stricmp(Op, "<=") == 0) { Status = Size <= Value; } else if (_stricmp(Op, ">=") == 0) { Status = Size >= Value; } DeleteArray(Op); } } if (Field == FIELD_TEXT || Field < 0) { StrField(mail->GetBody(), FIELD_TEXT); StrField(mail->GetHtml(), FIELD_ALTERNATE_HTML); } StrField(mail->GetSubject(), FIELD_SUBJECT); // FIXME: // StrField(mail->GetMessageId(false), FIELD_MESSAGE_ID); StrField(mail->GetInternetHeader(), FIELD_INTERNET_HEADER); StrField(mail->GetLabel(), FIELD_LABEL); if (Field == FIELD_TO || Field == FIELD_CC || Field < 0) { - GDataIt To = mail->GetTo(); + LDataIt To = mail->GetTo(); for (LDataPropI *a = To->First(); a; a = To->Next()) { Status |= MatchAddress(a, Text, Field == FIELD_CC); } } if (Field == FIELD_FROM || Field < 0) { Status |= MatchAddress(mail->GetFrom(), Text); } if (Field == FIELD_REPLY || Field < 0) { Status |= MatchAddress(mail->GetObject()->GetObj(FIELD_REPLY), Text); } if (Field == FIELD_ATTACHMENTS_NAME || Field < 0) { List Files; if (mail->GetAttachments(&Files)) { for (auto a: Files) { auto FileName = a->GetName(); Status |= MatchString(FileName, Text) != 0; } } } if (Field == FIELD_MEMBER_OF_GROUP || Field < 0) { auto From = mail->GetFrom(); if (From) { auto email = From->GetStr(FIELD_EMAIL); if (email && GroupMap) { auto grps = GroupMap->Find(email); if (grps) Status |= MatchString(grps->toString(), Text); } } } Result &= Status; } if (Colours.Length()) { auto Col = mail->GetMarkColour(); Result &= Colours.HasItem((uint32_t)Col); } return Result; } bool MatchFilter(Filter *filter) { if (!filter || !filter->GetObject() || SearchText.Length() == 0) return false; LArray Strs; auto Obj = filter->GetObject(); static int Fields[] = { FIELD_FILTER_NAME, FIELD_FILTER_SCRIPT, FIELD_FILTER_CONDITIONS_XML, FIELD_FILTER_ACTIONS_XML }; for (int i=0; iGetStr(Fields[i]); if (s) Strs.Add(s); } for (auto t: SearchText) { for (auto s : Strs) if (MatchString(s, t)) return true; } return false; } bool MatchContact(Contact *contact, int Field = -1) { if (!contact || !contact->GetObject() || SearchText.Length() == 0) return false; const char *v = 0; bool Status = true; for (unsigned i=0; i= 0) { ItemFieldDef *Def = GetFieldDefById(Field); if (Def) { if ((v = contact->GetObject()->GetStr(Def->FieldId))) { Found |= (stristr(v, Text) != 0); } } } else { for (ItemFieldDef *fd = ContactFieldDefs; !Found && fd->FieldId; fd++) { if ((v = contact->GetObject()->GetStr(fd->FieldId))) { Found |= stristr(v, Text) != 0; } } } Status &= Found; } return Status; } public: char Status[256]; FindThread( ScribeWnd *app, LView *notify, ScribeFolder *folder, ResultList *results, bool mail, int mailf, bool contact, int contactf, const char *searchText, bool deep, bool case_sensitive, bool match_word, LArray colours, LAutoPtr groupMap) : LThread("FindThread.Thread"), LMutex("FindThread.Mutex") { Loop = true; Status[0] = 0; App = app; Notify = notify; Folder = folder; Results = results; InMail = mail; MailF = mailf; InContacts = contact; ContactF = contactf; Deep = deep; CaseSensitive = case_sensitive; MatchWord = match_word; Colours = colours; ItemsSearched = 0; GroupMap = groupMap; if (searchText) { const char *White = " \t\r\n"; for (auto s = searchText; s && *s; ) { while (*s && strchr(White, *s)) s++; if (*s && strchr("\'\"", *s)) { char Delim = *s++; char *e = strchr(s, Delim); if (e) { SearchText.Add( NewStr(s, e-s) ); s = e + 1; } else { SearchText.Add( NewStr(s) ); break; } } else { const char *e = s; while (*e && !strchr(White, *e)) e++; SearchText.Add( NewStr(s, e-s) ); s = *e ? e + 1 : 0; } } } Loop = true; Run(); } ~FindThread() { Loop = false; while (!IsExited()) { LSleep(10); } SearchText.DeleteArrays(); } void Search(bool i) { Loop = i; } void SetNotify(LViewI *w) { Notify = w; } bool AddThing(Thing *T) { ResultItem *Item = 0; if (T && Results) { Results->Insert(Item = new ResultItem(App, T)); } return Item != 0; } void SearchFolder(ScribeFolder *Folder) { if (!Folder) return; Store3State State = (Store3State)Folder->GetObject()->GetInt(FIELD_LOADED); Folder->LoadThings(); if (Lock(_FL)) { CurFolder = Folder->GetName(true); Unlock(); } for (auto Item : Folder->Items) { if (!Loop) break; switch ((uint32_t)Item->Type()) { case MAGIC_MAIL: { if (InMail) { bool PreLoad = Item->GetObject() != 0; Mail *m = Item->IsMail(); if (m) { if (MatchMail(m, MailF)) { PreLoad = AddThing(m); break; } ItemsSearched++; } } break; } case MAGIC_CONTACT: { if (InContacts) { bool PreLoad = Item->GetObject() != 0; Contact *c = Item->IsContact(); if (c) { if (MatchContact(c, ContactF)) { PreLoad = AddThing(c); break; } ItemsSearched++; } } break; } case MAGIC_FILTER: { bool PreLoad = Item->GetObject() != 0; Filter *f = Item->IsFilter(); if (f) { if (MatchFilter(f)) { PreLoad = AddThing(f); break; } ItemsSearched++; } break; } } } if (Deep) { for (ScribeFolder *f = Folder->GetChildFolder(); Loop && f; f = f->GetNextFolder()) { f->LoadThings(Notify); SearchFolder(f); } } if (State == Store3Unloaded) { // FIXME, should we unload the folder here? } } LString GetStatus() { char s[256]; double Sec = (double)(LCurrentTime() - StartTime) / 1000.0; double Rate = Sec != 0.0 ? ItemsSearched / Sec : 0.0; if (Lock(_FL)) { sprintf_s(s, sizeof(s), "Searching '%s', %.1f items/s.", CurFolder.Get(), Rate); Unlock(); } return s; } int Main() { StartTime = LCurrentTime(); ItemsSearched = 0; SearchFolder(Folder); if (Notify) { Notify->PostEvent(M_END_SEARCH); } return 0; } }; /////////////////////////////////////////////////////////////////////// class FindWnd : public LWindow, public LResourceLoad, public LDataEventsI { ScribeWnd *App; LEdit *Text; LButton *Search; LEdit *Folder; LCheckBox *SearchSub; LCheckBox *SearchMail; LCombo *MailField; LCheckBox *SearchContact; LCombo *ContactField; ResultList *Results; LArray MailFieldIds; char *SearchBtnText; FindThread *Thread; void OnSearch(bool Searching); public: FindWnd(ScribeWnd *app, ScribeFolder *folder); ~FindWnd(); int OnNotify(LViewI *Col, LNotification n); LMessage::Result OnEvent(LMessage *m); void OnNew(LDataFolderI *parent, LArray &new_items, int pos, bool is_new); bool OnDelete(LDataFolderI *parent, LArray &items); bool OnMove(LDataFolderI *new_parent, LDataFolderI *old_parent, LArray &items); bool OnChange(LArray &items, int FieldHint); void OnPulse(); }; FindWnd::FindWnd(ScribeWnd *app, ScribeFolder *folder) { App = app; Thread = 0; Results = 0; SearchBtnText = 0; SetQuitOnClose(false); // Setup controls LAutoString ResName; LRect r; if (LoadFromResource(IDD_FIND, this, &r, &ResName)) { SetPos(r); Name(ResName); GetViewById(IDC_TEXT, Text); GetViewById(IDOK, Search); GetViewById(IDC_FOLDER, Folder); GetViewById(IDC_SEARCH_SUB, SearchSub); GetViewById(IDC_MAIL, SearchMail); GetViewById(IDC_MAIL_FIELD, MailField); GetViewById(IDC_CONTACT, SearchContact); GetViewById(IDC_CONTACT_FIELD, ContactField); if (Search) { SearchBtnText = NewStr(Search->Name()); } LTableLayout *Tbl; if (GetViewById(IDC_TABLE, Tbl)) { auto *c = Tbl->GetCell(0, Tbl->CellY()-1); c->Add(Results = new ResultList(IDC_RESULTS, 10, 260, 400, 300, "")); } // Initialize controls if (folder) { auto FolderPath = folder->GetPath(); if (FolderPath) SetCtrlName(IDC_FOLDER, FolderPath); } else { SetCtrlName(IDC_FOLDER, "/"); } char n[256]; sprintf_s(n, sizeof(n), "(%s)", LLoadString(IDS_NONE)); if (MailField) { MailField->Insert(n); for (ItemFieldDef *Def = MailFieldDefs; Def->FieldId; Def++) { const char *FName = LLoadString(Def->FieldId); if (FName) { MailField->Insert(FName); MailFieldIds.Add(Def->FieldId); } } /* Unsupported yet... MailField->Insert(LLoadString(IDS_ATTACHMENTS_DATA)); MailFieldIds.Add(FIELD_ATTACHMENTS_DATA); */ MailField->Insert(LLoadString(IDS_ATTACHMENTS_NAME)); MailFieldIds.Add(FIELD_ATTACHMENTS_NAME); MailField->Insert(LLoadString(IDS_MEMBER_OF_GROUP)); MailFieldIds.Add(FIELD_MEMBER_OF_GROUP); } if (ContactField) { ContactField->Insert(n); for (ItemFieldDef *Def = ContactFieldDefs; Def->CtrlId; Def++) { ContactField->Insert(LLoadString(Def->FieldId)); } } if (Results) { Results->AskText(true); Results->SetImageList(App->GetIconImgList(), false); switch ((uint32_t)folder->GetItemType()) { case MAGIC_MAIL: Results->AddColumn(LLoadString(Mail::DefaultMailFields[0]), 100); Results->AddColumn(LLoadString(Mail::DefaultMailFields[1]), 100); Results->AddColumn(LLoadString(Mail::DefaultMailFields[2]), 100); Results->AddColumn(LLoadString(Mail::DefaultMailFields[3]), 100); break; case MAGIC_CONTACT: Results->AddColumn(LLoadString(Contact::DefaultContactFields[0]), 100); Results->AddColumn(LLoadString(Contact::DefaultContactFields[1]), 100); Results->AddColumn(LLoadString(Contact::DefaultContactFields[2]), 100); Results->AddColumn("", 100); break; default: Results->AddColumn("", 100); Results->AddColumn("", 100); Results->AddColumn("", 100); Results->AddColumn("", 100); break; } Results->AddColumn(LLoadString(IDS_1062), 200); } SetCtrlValue(IDC_SEARCH_SUB, false); switch ((uint32_t)folder->GetItemType()) { case MAGIC_MAIL: SetCtrlValue(IDC_MAIL, true); SetCtrlValue(IDC_CONTACT, false); break; case MAGIC_CONTACT: SetCtrlValue(IDC_MAIL, false); SetCtrlValue(IDC_CONTACT, true); break; default: SetCtrlValue(IDC_MAIL, true); SetCtrlValue(IDC_CONTACT, true); break; } MoveSameScreen(App); // Create window if (Attach(0)) { AttachChildren(); if (Search) Search->Default(true); if (Folder) Folder->Enabled(false); Visible(true); Text->Focus(true); } } App->AddStore3EventHandler(this); } FindWnd::~FindWnd() { App->RemoveStore3EventHandler(this); DeleteObj(Thread); DeleteArray(SearchBtnText); } void FindWnd::OnSearch(bool Searching) { Text->Enabled(!Searching); Folder->Enabled(!Searching); SearchSub->Enabled(!Searching); SearchMail->Enabled(!Searching); MailField->Enabled(!Searching); SearchContact->Enabled(!Searching); ContactField->Enabled(!Searching); // Results->Enabled(!Searching); MailField->Invalidate(); ContactField->Invalidate(); SetPulse(Searching ? 500 : -1); if (Searching) { Search->Name(LLoadString(IDS_CANCEL)); } else { Search->Name(SearchBtnText ? SearchBtnText : (char*)"Search"); } } int FindWnd::OnNotify(LViewI *Col, LNotification n) { switch (Col->GetId()) { case IDC_PICK_FOLDER: { if (Folder) { auto Dlg = new FolderDlg(this, App); Dlg->DoModal([this, Dlg](auto dlg, auto id) { if (id) this->Folder->Name(Dlg->Get()); delete dlg; }); } break; } case IDOK: { if (Results && Folder) { if (Thread) { Thread->Search(false); Search->Name("Ending..."); } else { Results->Empty(); int MailF = -1; int ContactF = -1; if (MailField) { int Index = (int)MailField->Value(); if (Index > 0) { Index--; if (Index < (int)MailFieldIds.Length()) { MailF = MailFieldIds[Index]; } } } if (ContactField) { int Index = (int) ContactField->Value(); if (Index > 0) { ContactF = ContactFieldDefs[Index-1].FieldId; } } ScribeFolder *Root = App->GetFolder(Folder->Name()); if (Root) { auto it = Root->Items.begin(); LProgressDlg prog(this, 1000); prog.SetDescription("Get message id's..."); prog.SetRange(Root->Items.Length()); int n = 0; for (Thing *t = *it; t; t = *++it, n++) { Mail *m = t->IsMail(); if (m) { // This can't be done in the thread, so do it here m->GetMessageId(); } prog.Value(n); if (n % 10 == 0) LYield(); } LArray Colours; LMarkColourSelect *Mcs; if (GetViewById(IDC_COLOUR, Mcs)) { for (int i=0; iColSel); i++) { if (Mcs->ColSel[i]) Colours.Add(MarkColours32[i]); } } Thread = new FindThread(App, this, Root, Results, SearchMail && SearchMail->Value() > 0, MailF, SearchContact && SearchContact->Value() > 0, ContactF, Text->Name(), SearchSub && SearchSub->Value(), GetCtrlValue(IDC_FIND_CASE) != 0, GetCtrlValue(IDC_FIND_WORD) != 0, Colours, LAutoPtr(new LGroupMap(App))); OnSearch(true); } } } break; } } return 0; } LMessage::Result FindWnd::OnEvent(LMessage *m) { switch (m->Msg()) { case M_END_SEARCH: { // thread is ended, it will delete itself OnSearch(false); DeleteObj(Thread); break; } } return LWindow::OnEvent(m); } void FindWnd::OnNew(LDataFolderI *parent, LArray &new_items, int pos, bool is_new) { } bool FindWnd::OnDelete(LDataFolderI *parent, LArray &items) { if (Results) { LHashTbl, LListItem*> Map; List a; if (Results->GetAll(a)) { for (auto i: a) { Map.Add(i->GetThing()->GetObject(), i); } } for (unsigned i=0; i &items) { if (Results) { LHashTbl, LListItem*> Map; List a; if (Results->GetAll(a)) { for (auto i: a) { Map.Add(i->GetThing()->GetObject(), i); } } for (unsigned i=0; iUpdate(); } } } return true; } void FindWnd::OnPulse() { LViewI *v; if (!Thread || !GetViewById(IDC_STATUS, v)) return; v->Name(Thread->GetStatus()); v->SendNotify(LNotifyTableLayoutRefresh); } bool FindWnd::OnChange(LArray &items, int FieldHint) { return true; } LView *OpenFinder(ScribeWnd *App, ScribeFolder *Folder) { return new FindWnd(App, Folder); } diff --git a/Code/ScribeFolder.cpp b/Code/ScribeFolder.cpp --- a/Code/ScribeFolder.cpp +++ b/Code/ScribeFolder.cpp @@ -1,4554 +1,4486 @@ /* ** FILE: ScribeFolder.cpp ** AUTHOR: Matthew Allen ** DATE: 17/1/2000 ** DESCRIPTION: Scribe folder's ** ** Copyright (C) 2000-2002, Matthew Allen ** fret@memecode.com */ // Includes #include "Scribe.h" #include "lgi/common/DropFiles.h" #include "lgi/common/ProgressDlg.h" #include "lgi/common/LgiQuickSort.h" #include "lgi/common/DisplayString.h" #include "lgi/common/TextFile.h" #include "lgi/common/LgiRes.h" #include "lgi/common/FileSelect.h" #include "lgi/common/ClipBoard.h" #include "Calendar.h" #include "resdefs.h" #include "ReplicateDlg.h" +#include "FolderTask.h" ////////////////////////////////////////////////////////////////////////////// #if WINNATIVE #include "lgi/common/Com.h" #endif class LDndFilePromise #if defined(WINDOWS) #elif defined(__GTK_H__) #elif defined(MAC) #endif { LStream *Src; #if WINNATIVE FILEGROUPDESCRIPTOR Gd; #elif defined(__GTK_H__) #elif defined(MAC) #endif public: LDndFilePromise(LDragData &dd, LStream *src, LString FileName) { Src = src; #if WINNATIVE ZeroObj(Gd); Gd.cItems = 1; // CLSID Fd.clsid; // SIZEL Fd.sizel; // POINTL Fd.pointl; // FILETIME Fd.ftCreationTime; // FILETIME Fd.ftLastAccessTime; // FILETIME Fd.ftLastWriteTime; FILEDESCRIPTOR &Fd = Gd.fgd[0]; Fd.dwFlags = FD_ATTRIBUTES | FD_PROGRESSUI; Fd.dwFileAttributes = FILE_ATTRIBUTE_COMPRESSED | FILE_ATTRIBUTE_NORMAL; int64 Sz = Src->GetSize(); if (Sz >= 0) { Fd.dwFlags |= FD_FILESIZE; Fd.nFileSizeHigh = Sz >> 32; Fd.nFileSizeLow = Sz & 0xffffffff; } auto Leaf = FileName.RFind(DIR_STR); LAutoWString FnW(Utf8ToWide(Leaf >= 0 ? FileName(Leaf+1,-1) : FileName)); Strcpy(Fd.cFileName, CountOf(Fd.cFileName), FnW.Get()); LVariant &v = dd.Data[0]; v.SetBinary(sizeof(Gd), &Gd); #elif defined(__GTK_H__) LAssert(!"Not impl."); #elif LGI_COCOA LAssert(!"Not impl."); #elif LGI_CARBON // See Apple: Technical Note TN1085 // https://web.archive.org/web/20080725134839/http://developer.apple.com/technotes/tn/tn1085.html // flavorTypePromiseHFS = 'phfs' PromiseHFSFlavor Promise; ZeroObj(Promise); Promise.fileType = 'mbox'; Promise.fileCreator = '****'; Promise.promisedFlavor = kDragPromisedFlavor; // 'fssP' int Sz = sizeof(Promise); LVariant &v1 = dd.Data[0]; v1.SetBinary(sizeof(Promise), &Promise, false); // The Dnd code will add the 2nd part of the promise // There isn't the visibility into that API at this level #else #error "Impl me." #endif } ~LDndFilePromise() { #if defined(WINDOWS) #elif defined(__GTK_H__) #elif defined(MAC) #endif } #if defined(WINDOWS) #elif defined(__GTK_H__) #elif defined(MAC) #endif int AddContents(LDragData &dd) { dd.Data[0] = Src; return 1; } }; ////////////////////////////////////////////////////////////////////////////// #define PROFILE_POPULATE 0 #define PROFILE_LOAD_THINGS 0 int FoldersCurrentlyLoading = 0; char ScribeFolderObject[] = "com.memecode.Folder"; class ScribeFolderPriv { public: int8 IsInbox = -1; bool InUpdateUnread = false; LAutoPtr DsBase; LAutoPtr DsUnread; LAutoPtr FilePromise; }; ////////////////////////////////////////////////////////////////////////////// ScribeFolder::ScribeFolder() { d = new ScribeFolderPriv; } ScribeFolder::~ScribeFolder() { bool IsRoot = !GetParent(); if (CurState != FldState_Idle) { // int Cur = FoldersCurrentlyLoading; #ifdef _DEBUG LAssert(!"Can't delete folder while it's busy."); #else LgiMsg( App, "Can't unload folder while busy. Please report what\n" "you were attempting to do to fret@memecode.com", "Scribe Error", MB_OK); #endif return; } switch (GetItemType()) { case MAGIC_CALENDAR: CalendarSource::FolderDelete(this); // fall through case MAGIC_CONTACT: case MAGIC_FILTER: case MAGIC_GROUP: App->RemoveThingSrc(this); break; default: break; } if (View()) { bool IsMe = View()->GetContainer() == this; // bool IsSelect = Select(); View()->DeletePlaceHolders(); if (IsMe) { View()->SetContainer(0); View()->RemoveAll(); } } Thing *t; int i = 0; while ((t = Items[i])) { if (!t->DecRef()) i++; } Update(); EmptyFieldList(); DeleteObj(d); ScribeFolder *f = GetChildFolder(); DeleteObj(f); if (!IsRoot) { f = GetNextFolder(); DeleteObj(f); } // Don't delete 'Object' here, it's owned by the backend storage object } bool ScribeFolder::SetObject(LDataI *o, bool InDestructor, const char *File, int Line) { if (CurState != FldState_Idle) { LAssert(!"Can't set object while folder is not idle."); return false; } return LDataUserI::SetObject(o, InDestructor, File, Line); } void ScribeFolder::UpdateOsUnread() { #ifdef WIN32 if (App->GetFolder(FOLDER_INBOX) == this) { LHashTbl, int> Un(0, -1); // Pre-populate 'Un' with our email accounts List *Acc = App->GetAccounts(); if (Acc) { for (auto a: *Acc) { LVariant e = a->Identity.Email(); if (e.Str()) Un.Add(e.Str(), 0); } } // Scan folder for unread email... for (auto t : Items) { Mail *m = t->IsMail(); if (m) { if (!TestFlag(m->GetFlags(), MAIL_READ)) { ScribeAccount *a = m->GetAccountSentTo(); if (a) { LVariant Email; Email = a->Identity.Email(); if (Email.Str()) { int Cur = Un.Find(Email.Str()); Un.Add(Email.Str(), Cur + 1); } } } } } #if 0 // Set system email status LArray Ver; int Os = LGetOs(&Ver); if ((Os == LGI_OS_WIN32 || Os == LGI_OS_WIN64) && (Ver[0] > 5 || (Ver[0] == 5 && Ver[1] > 0))) { char e[256]; LgiGetExeFile(e, sizeof(e)); typedef HRESULT (__stdcall *pSHSetUnreadMailCount)(LPCWSTR pszMailAddress, DWORD dwCount,LPCWSTR pszShellExecuteCommand); LLibrary Shell32("shell32"); pSHSetUnreadMailCount SHSetUnreadMailCount = (pSHSetUnreadMailCount) Shell32.GetAddress("SHSetUnreadMailCountW"); if (SHSetUnreadMailCount) { LVariant Exe; Exe = e; char *k; for (int u=Un.First(&k); u>=0; u=Un.Next(&k)) { LVariant Email = k; SHSetUnreadMailCount(Email.WStr(), u, Exe.WStr()); } } } #endif } #endif } void ScribeFolder::SetLoadOnDemand() { if ((LoadOnDemand = new LTreeItem)) { Expanded(false); LoadOnDemand->SetText((char*)LLoadString(IDS_LOADING)); Insert(LoadOnDemand); } } void ScribeFolder::GetMessageById(const char *Id, std::function Callback) { if (!Id) { if (Callback) Callback(NULL); return; } LoadThings(NULL, [&](auto s) { if (s <= Store3Loading) { if (Callback) Callback(NULL); return; } for (auto t : Items) { Mail *r = t->IsMail(); if (!r) continue; auto rid = r->GetMessageId(); if (!Stricmp(rid, Id)) { if (Callback) Callback(r); return; } } }); } bool ScribeFolder::InsertThing(Thing *t) { bool Status = false; if (t && Select() && View()) { // Filter ThingFilter *Filter = App->GetThingFilter(); t->SetFieldArray(FieldArray); if (!Filter || Filter->TestThing(t)) { if (t->GetList() != View()) { View()->Insert(t); #if WINNATIVE UpdateWindow(View()->Handle()); #endif } } Status = true; } return Status; } bool ScribeFolder::GetThreaded() { return GetObject() ? GetObject()->GetInt(FIELD_FOLDER_THREAD) != 0 : false; } void ScribeFolder::SetThreaded(bool t) { if (GetObject() && GetThreaded() ^ t) { GetObject()->SetInt(FIELD_FOLDER_THREAD, t); SetDirty(); } } ThingList *ScribeFolder::View() { return App ? App->GetMailList() : 0; } bool ScribeFolder::HasFieldId(int Id) { auto o = GetFldObj(); if (o) { for (LDataPropI *f = o->Fields().First(); f; f = o->Fields().Next()) { if (Id == f->GetInt(FIELD_ID)) { return true; } } } return false; } void ScribeFolder::SetFolderPerms(LView *Parent, ScribeAccessType Access, ScribePerm Perm, std::function Callback) { ScribePerm Current = GetFolderPerms(Access); int Field = (Access == ScribeReadAccess) ? FIELD_FOLDER_PERM_READ : FIELD_FOLDER_PERM_WRITE; if (GetObject() && Current != Perm) { App->GetAccessLevel(Parent, Current, GetPath(), [&](auto Allow) { if (Allow) { GetObject()->SetInt(Field, Perm); SetDirty(); } if (Callback) Callback(Allow); }); } else { if (Callback) Callback(true); // i.e. not changing } } ScribePerm ScribeFolder::GetFolderPerms(ScribeAccessType Access) { int Field = (Access == ScribeReadAccess) ? FIELD_FOLDER_PERM_READ : FIELD_FOLDER_PERM_WRITE; return GetObject() ? (ScribePerm)GetObject()->GetInt(Field) : PermRequireNone; } Store3Status ScribeFolder::CopyTo(ScribeFolder *NewParent, int NewIndex) { Store3Status Copied = Store3Error; if (NewParent == GetParent()) { NewParent->GetObject()->GetStore(); } else if (GetObject() && GetObject()->GetStore() && NewParent->GetObject() && NewParent->GetObject()->GetStore()) { // Find or create the destination folder LDataFolderI *Dst = 0; auto Name = GetObject()->GetStr(FIELD_FOLDER_NAME); for (ScribeFolder *f = NewParent->GetChildFolder(); f; f = f->GetNextFolder()) { auto n = f->GetObject()->GetStr(FIELD_FOLDER_NAME); if (n && !_stricmp(n, Name)) { Dst = f->GetFldObj(); } } if (!Dst) { // Create sub-folder if ((Dst = dynamic_cast(NewParent->GetObject()->GetStore()->Create(GetObject()->Type())))) { Dst->CopyProps(*GetObject()); if (Dst->Save(NewParent->GetObject()) == Store3Error) return Store3Error; } else return Store3Error; } if (Dst) { Copied = Store3ReplicateFolders(App, Dst, GetFldObj(), true, false, 0); } } else LAssert(!"Pointer error"); return Copied; } Store3Status ScribeFolder::SetFolder(ScribeFolder *f, int Param) { Store3Status Moved = Store3Error; if (f == this) { LAssert(0); } else if (f && GetObject() && GetObject()->GetStore() && f->GetObject() && f->GetObject()->GetStore()) { if (GetObject()->GetStore() == f->GetObject()->GetStore()) { // Simple in storage movement LArray Mv; Mv.Add(GetObject()); Moved = GetObject()->GetStore()->Move(f->GetFldObj(), Mv); if (Moved && Param >= 0) { GetObject()->SetInt(FIELD_FOLDER_INDEX, Param); } } else { // Cross storage movement... // Find or create the destinate folder LDataFolderI *Dst = 0; int Idx = 0; auto Name = GetObject()->GetStr(FIELD_FOLDER_NAME); for (ScribeFolder *c = f->GetChildFolder(); c; c = c->GetNextFolder(), Idx++) { auto n = c->GetObject()->GetStr(FIELD_FOLDER_NAME); if (n && !_stricmp(n, Name)) { Dst = c->GetFldObj(); break; } } if (!Dst) { Dst = dynamic_cast(f->GetObject()->GetStore()->Create(f->Type())); } else { ScribeFolder *c = CastFolder(dynamic_cast(Dst)); if (c) { c->Remove(); Param = Idx; DeleteObj(c); } } if (Dst) { if (View()) View()->RemoveAll(); // Clean up all the items and sub-folders, they will be created by the // replication code calling "OnNew". Thing *t; while ((t = Items[0])) { t->DecRef(); Items.Delete(t); } // Delete the child folders... ScribeFolder *Cf; while ((Cf = GetChildFolder())) { DeleteObj(Cf); } // Copy ourself over... Dst->CopyProps(*GetObject()); LDataFolderI *Old = GetFldObj(); SetObject(Dst, false, _FL); // Save the object to the new store... Store3Status s = GetObject()->Save(f->GetObject()); if (s != Store3Error) { // And replicate all the children objects... Moved = Store3ReplicateFolders(App, GetFldObj(), Old, true, true, 0); } } else LAssert(!"Not a valid folder"); } if (Moved == Store3Success) { // Move LTreeItem node... f->Insert(this, Param); Select(true); LoadFolders(); // Adjust all the other indexes so that it's remembered ScribeFolder *p = GetFolder(); if (p) { int Idx = 0; for (p = p->GetChildFolder(); p; p = p->GetNextFolder()) { p->SetSortIndex(Idx++); p->SetDirty(); } } } } else LAssert(!"Pointer error"); return Moved; } Store3Status ScribeFolder::DeleteAllThings(std::function Callback) { if (!GetFldObj()) return Store3Error; Store3Status r = GetFldObj()->DeleteAllChildren(); if (r == Store3Error) { LAssert(!"DeleteAllChildren failed."); } else if (r == Store3Success) { LAssert(Items.Length() == 0); OnUpdateUnRead(0, true); Update(); } return r; } Store3Status ScribeFolder::DeleteThing(Thing *t, std::function Callback) { Store3Status Status = Store3Error; if (t && t->GetObject()) { if (t->IsAttachment()) { #ifdef _DEBUG Mail *m = #endif t->IsAttachment()->GetOwner(); LAssert(m && Items.HasItem(m)); } if (t->GetObject()->Delete()) { t->SetParentFolder(NULL); if (View()) View()->Remove(t); } } return Status; } Store3Status ScribeFolder::WriteThing(Thing *t, std::function Callback) { if (!t) { if (Callback) Callback(Store3Error); return Store3Error; } - auto Path = GetFolder()->GetPath(); - - auto OnAllow = [&]() + auto ParentFolder = GetFolder(); + auto Path = ParentFolder ? ParentFolder->GetPath() : NULL; + + auto OnAllow = [this, t, Callback]() { // Generic thing storage.. bool Create = !t->GetObject(); if (Create) t->SetObject(GetObject()->GetStore()->Create(t->Type()), false, _FL); if (!t->GetObject()) { LAssert(!"No object?"); LgiTrace("%s:%i - No object to save.\n", _FL); if (Callback) Callback(Store3Error); return; } // saving a thing that already has an item on disk auto Obj = GetObject(); auto Status = t->GetObject()->Save(Obj); if (Status != Store3Error) { // The ScribeWnd::OnNew will take care of inserting the item into the // right folder, updating the unread count, any filtering etc. t->OnSerialize(true); if (Callback) Callback(Status); } else { if (Create) t->SetObject(NULL, false, _FL); LgiTrace("%s:%i - Object->Save returned %i.\n", _FL, Status); if (Callback) Callback(Store3Error); } }; if (App) - App->GetAccessLevel(App, GetWriteAccess(), Path, [&](auto Allow) - { - if (Allow) - OnAllow(); - }); + App->GetAccessLevel(App, + GetWriteAccess(), + Path, + [OnAllow](auto Allow) + { + if (Allow) + OnAllow(); + }); else OnAllow(); return Store3Success; } int ThingContainerNameCmp(LTreeItem *a, LTreeItem *b, NativeInt d) { ScribeFolder *A = dynamic_cast(a); ScribeFolder *B = dynamic_cast(b); if (A && B) { const char *s1 = A->GetText(); const char *s2 = B->GetText(); const char *Empty = ""; return _stricmp(s1?s1:Empty, s2?s2:Empty); } else LAssert(!"Invalid objects."); return 0; } int ThingContainerIdxCmp(LTreeItem *a, LTreeItem *b, NativeInt d) { ScribeFolder *A = dynamic_cast(a); ScribeFolder *B = dynamic_cast(b); if (A && B) { int Aidx = A->GetSortIndex(); int Bidx = B->GetSortIndex(); if (Aidx >= 0 || Bidx >= 0) { return Aidx - Bidx; } } else LAssert(!"Invalid objects."); return 0; } void ScribeFolder::SortSubfolders() { int i = 0; ScribeFolder *c; LTreeItem::Items.Sort(ThingContainerNameCmp); for (c = GetChildFolder(); c; c = c->GetNextFolder()) { c->SetSortIndex(i++); c->SetDirty(); } GetTree()->UpdateAllItems(); GetTree()->Invalidate(); } void ScribeFolder::DoContextMenu(LMouse &m) { MailTree *mt = dynamic_cast(GetTree()); LScriptUi s(new LSubMenu); List Templates; bool ForceDelete = m.Shift(); bool IsTrash = false; bool IsSystem = false; if (!s.Sub) return; IsTrash = GetItemType() == MAGIC_ANY; IsSystem = App->GetFolderType(this) >= 0; if (App->GetFolderType(this) == FOLDER_OUTBOX) { s.Sub->AppendItem(LLoadString(IDS_MARK_ALL_SEND), IDM_MARK_SEND, true); } if (IsTrash) { s.Sub->AppendItem(LLoadString(IDS_EMPTY), IDM_EMPTY, true); s.Sub->AppendItem(LLoadString(IDS_MARK_ALL_READ), IDM_MARK_ALL_READ, true); } else if (!IsRoot()) { auto *LiteralNew = LLoadString(IDS_NEW); const char *Type = 0; switch (GetItemType()) { case MAGIC_MAIL: Type = LLoadString(IDS_MAIL); break; case MAGIC_CONTACT: Type = LLoadString(IDS_CONTACT); break; case MAGIC_CALENDAR: Type = LLoadString(IDS_CAL_EVENT); break; case MAGIC_FILTER: Type = LLoadString(IDS_FILTER); break; default: break; } if (Type) { char Str[256]; sprintf_s(Str, sizeof(Str), "%s %s", LiteralNew, Type); s.Sub->AppendItem(Str, IDM_NEW_EMAIL, true); } } switch (GetItemType()) { case MAGIC_CONTACT: { char Str[256]; const char *LiteralEmail = LLoadString(IDS_EMAIL); sprintf_s(Str, sizeof(Str), "%s '%s'", LiteralEmail, GetText()); s.Sub->AppendItem(Str, IDM_EMAIL_GROUP, true); ScribeFolder *f = App->GetFolder(FOLDER_TEMPLATES); if (f) { // FIXME f->LoadThings(); auto Merge = s.Sub->AppendSub(LLoadString(IDS_MERGE_TEMPLATE)); if (Merge) { int n = 0; for (auto t: f->Items) { Mail *m = t->IsMail(); if (m) { Templates.Insert(m); Merge->AppendItem(m->GetSubject()?m->GetSubject():(char*)"(no subject)", IDM_MERGE_TEMPLATE_BASE+n++, true); } } } } else { s.Sub->AppendItem(LLoadString(IDS_MERGE_TEMPLATE), 0, false); } s.Sub->AppendItem(LLoadString(IDS_MERGE_FILE), IDM_MERGE_FILE, true); break; } case MAGIC_MAIL: { if (!IsRoot()) { s.Sub->AppendItem(LLoadString(IDC_COLLECT_SUB_MAIL), IDM_COLLECT_MAIL, true); s.Sub->AppendItem(LLoadString(IDS_MARK_ALL_READ), IDM_MARK_ALL_READ, true); if (GetObject() && GetObject()->GetInt(FIELD_STORE_TYPE) == Store3Imap) { s.Sub->AppendItem(LLoadString(IDC_UNDELETE), IDM_UNDELETE, true); s.Sub->AppendItem("Expunge", IDM_EXPUNGE, true); s.Sub->AppendItem(LLoadString(IDC_REFRESH), IDM_REFRESH, true); } } break; } default: break; } if (s.Sub->ItemAt(0)) { s.Sub->AppendSeparator(); } s.Sub->AppendItem(LLoadString(IDS_FIND), IDM_FIND, true); s.Sub->AppendSeparator(); bool HasFolderTask = App->HasFolderTasks(); bool FolderOp = !HasFolderTask && !IsRoot(); s.Sub->AppendItem(LLoadString(IDS_DELETEFOLDER), IDM_DELETE, FolderOp && ((!IsTrash && !IsSystem) || ForceDelete)); s.Sub->AppendItem(LLoadString(IDS_CREATESUBFOLDER), IDM_CREATE_SUB, !HasFolderTask && CanHaveSubFolders(GetItemType())); s.Sub->AppendItem(LLoadString(IDS_RENAMEFOLDER), IDM_RENAME, FolderOp); auto ExportMimeType = GetStorageMimeType(); s.Sub->AppendItem(LLoadString(IDS_EXPORT), IDM_EXPORT, FolderOp && ExportMimeType != NULL); s.Sub->AppendSeparator(); s.Sub->AppendItem(LLoadString(IDS_SORT_SUBFOLDERS), IDM_SORT_SUBFOLDERS, true); s.Sub->AppendItem(LLoadString(IDS_COPY_PATH), IDM_COPY_PATH, true); s.Sub->AppendItem(LLoadString(IDS_PROPERTIES), IDM_PROPERTIES, true); LArray Callbacks; if (App->GetScriptCallbacks(LFolderContextMenu, Callbacks)) { LScriptArguments Args(NULL); Args[0] = new LVariant(App); Args[1] = new LVariant(this); Args[2] = new LVariant(&s); for (auto c: Callbacks) App->ExecuteScriptCallback(*c, Args); Args.DeleteObjects(); } m.ToScreen(); LPoint Scr = _ScrollPos(); m.x -= Scr.x; m.y -= Scr.y; int Msg = s.Sub->Float(GetTree(), m.x, m.y); switch (Msg) { case IDM_NEW_EMAIL: { switch (GetItemType()) { case MAGIC_MAIL: case MAGIC_CONTACT: case MAGIC_CALENDAR: case MAGIC_FILTER: { if (App) App->CreateItem(GetItemType(), this); break; } default: break; } break; } case IDM_MARK_SEND: { for (Thing *i: Items) { Mail *m = i->IsMail(); if (m) { m->SetFlags(MAIL_READY_TO_SEND | MAIL_READ | MAIL_CREATED); m->SetDirty(); m->Update(); } } break; } case IDM_EMAIL_GROUP: { if (App) { Mail *m = dynamic_cast(App->CreateItem(MAGIC_MAIL, NULL, false)); if (m) { MailUi *UI = dynamic_cast(m->DoUI()); if (UI) { List Cts; App->GetContacts(Cts, this); for (auto c: Cts) { UI->AddRecipient(c); } } } } break; } case IDM_FIND: { ScribeFolder *Folder = (ScribeFolder*) GetTree()->Selection(); OpenFinder(App, Folder); break; } case IDM_CREATE_SUB: { if (mt) mt->OnCreateSubDirectory(this); break; } case IDM_DELETE: { if (mt) mt->OnDelete(this, ForceDelete); break; } case IDM_RENAME: { auto Dlg = new FolderNameDlg(mt, GetName(true)); Dlg->DoModal([this, Dlg, mt](auto dlg, auto id) { if (id && ValidStr(Dlg->Name)) { // check for folder name conflicts... ScribeFolder *ParentFolder = GetFolder(); LString Path; if (ParentFolder) Path = ParentFolder->GetPath(); if (Path) { char s[256]; sprintf_s(s, sizeof(s), "%s/%s", Path.Get(), Dlg->Name); if (App->GetFolder(s)) { LgiMsg(mt, LLoadString(IDS_SUBFLD_NAME_CLASH), AppName, MB_OK); return; } } // change the folders name... OnRename(Dlg->Name); } delete dlg; }); break; } case IDM_EXPORT: { LString DropName = LGetLeaf(GetDropFileName()); if (!DropName) { LgiTrace("%s:%i - Failed to create folder name.\n", _FL); break; } auto s = new LFileSelect(mt); s->Name(DropName); s->Save([&](auto dlg, auto status) { LAutoPtr mem(dlg); if (status) { if (LFileExists(s->Name())) { LString a, b; a.Printf(LLoadString(IDS_ERROR_FILE_EXISTS), s->Name()); b.Printf("\n%s\n", LLoadString(IDS_ERROR_FILE_OVERWRITE)); if (LgiMsg(GetTree(), a + b, AppName, MB_YESNO) == IDNO) return; } LAutoPtr f(new LFile); if (!f || !f->Open(s->Name(), O_WRITE)) { LgiTrace("%s:%i - Failed to open '%s' for writing.\n", _FL, s->Name()); return; } f->SetSize(0); LAutoPtr str(f.Release()); ExportAsync(str, ExportMimeType); } }); break; } case IDM_EMPTY: { if (App->GetMailList()) { App->GetMailList()->RemoveAll(); LArray Del; for (ScribeFolder *c = GetChildFolder(); c; c = c->GetNextFolder()) Del.Add(c->GetObject()); if (Del.Length()) GetObject()->GetStore()->Delete(Del, false); DeleteAllThings([&](auto status) { mt->Invalidate(); }); } break; } case IDM_SORT_SUBFOLDERS: { SortSubfolders(); break; } case IDM_PROPERTIES: { mt->OnProperties(this); break; } case IDM_COPY_PATH: { LClipBoard c(GetTree()); #ifdef WINDOWS LAutoWString w(Utf8ToWide(GetPath())); c.TextW(w); #else c.Text(GetPath()); #endif break; } case IDM_COLLECT_MAIL: { CollectSubFolderMail(); break; } case IDM_MARK_ALL_READ: { LArray Change; for (auto c : Items) { Mail *m = c->IsMail(); if (m) { if (!TestFlag(m->GetFlags(), MAIL_READ)) Change.Add(m->GetObject()); } } LVariant v = MAIL_READ; if (GetObject()->GetStore()->Change(Change, FIELD_FLAGS, v, OpPlusEquals) == Store3Error) { LProgressDlg Prog(GetTree(), 500); Prog.SetRange(Change.Length()); // FIXME!! // Prog.SetYieldTime(200); Prog.SetDescription("Marking email..."); for (auto c : Change) { auto t = CastThing(c); Mail *m = t ? t->IsMail() : NULL; if (m) m->SetFlags(m->GetFlags() | MAIL_READ, false, false); Prog++; if (Prog.IsCancelled()) break; } OnUpdateUnRead(0, true); } break; } case IDM_MERGE_FILE: { auto s = new LFileSelect(mt); s->Type("Email Template", "*.txt;*.eml"); s->Open([this](auto dlg, auto id) { if (id) { LArray Recip; for (auto i: Items) { Recip.Add(new ListAddr(i->IsContact())); } App->MailMerge(Recip, dlg->Name(), 0); Recip.DeleteObjects(); } delete dlg; }); break; } case IDM_UNDELETE: { const char *s = DomToStr(SdUndelete); GetFldObj()->OnCommand(s); break; } case IDM_EXPUNGE: { const char *s = DomToStr(SdExpunge); GetFldObj()->OnCommand(s); break; } case IDM_REFRESH: { const char *s = DomToStr(SdRefresh); GetFldObj()->OnCommand(s); break; } default: { Mail *Template = Templates[Msg - IDM_MERGE_TEMPLATE_BASE]; if (Template) { LArray Recip; for (auto i: Items) { Recip.Add(new ListAddr(i->IsContact())); } App->MailMerge(Recip, 0, Template); Recip.DeleteObjects(); } else { // Handle any installed callbacks for menu items for (unsigned i=0; iExecuteScriptCallback(Cb, Args); } } } break; } } } bool ScribeFolder::IsInTrash() { ScribeFolder *p = this; while ((p = p->GetFolder())) { if (p->GetItemType() == MAGIC_ANY) return true; } return false; } /// This just adds certain folder types as a group ware source at the app level void ScribeFolder::OnItemType() { switch (GetItemType()) { case MAGIC_CONTACT: case MAGIC_FILTER: case MAGIC_CALENDAR: case MAGIC_GROUP: App->AddThingSrc(this); break; default: break; } } bool ScribeFolder::LoadFolders() { // static int64 Last = 0; auto FldObj = GetFldObj(); if (!FldObj) return true; CurState = FldState_Loading; FoldersCurrentlyLoading++; LHashTbl, ScribeFolder*> Loaded; for (ScribeFolder *c = GetChildFolder(); c; c = c->GetNextFolder()) { Loaded.Add(c->GetObject(), c); } LDataFolderI *s; auto &Sub = FldObj->SubFolders(); for (unsigned sIdx = 0; Sub.GetState() == Store3Loaded && sIdx < Sub.Length(); sIdx++) { if (!(s = Sub[sIdx])) break; if (!Loaded.Find(s)) { ScribeFolder *n = new ScribeFolder; if (n) { n->App = App; n->SetObject(s, false, _FL); Insert(n); SetWillDirty(false); Expanded(GetOpen() != 0); SetWillDirty(true); n->LoadFolders(); n->OnItemType(); } #if 0 int64 Now = LCurrentTime(); if (Now - Last > 500) { LYield(); Last = Now; } #endif } } LTreeItem::Items.Sort(ThingContainerIdxCmp); CurState = FldState_Idle; FoldersCurrentlyLoading--; return true; } class LoadProgressItem : public LListItem { int n; int Total; int64 Last; public: LoadProgressItem(int t) { n = 0; Total = t; Last = LCurrentTime(); } void SetPos(int i) { n = i; if (n % 32 == 0) { int64 Now = LCurrentTime(); if (Now > Last + 500) { if (Parent) { Parent->Invalidate(&Pos, true); #ifdef MAC LYield(); #endif } Last = Now; } LSleep(0); } } void OnPaint(ItemPaintCtx &Ctx) { int x = n * 150 / Total; LRect &r = Ctx; Ctx.pDC->Colour(L_FOCUS_SEL_BACK); Ctx.pDC->Rectangle(r.x1, r.y1, r.x1 + x, r.y2); Ctx.pDC->Colour(L_MED); Ctx.pDC->Rectangle(r.x1 + x + 1, r.y1, r.x1 + 150, r.y2); Ctx.pDC->Colour(L_WORKSPACE); Ctx.pDC->Rectangle(r.x1 + 151, r.y1, r.x2, r.y2); } }; class LoadingItem : public LListItem { LDataIterator *Iter; LString s; public: LoadingItem(LDataIterator *it) { _UserPtr = NULL; if ((Iter = it)) { Iter->SetProgressFn([this](ssize_t pos, ssize_t sz) { this->s.Printf("%.1f%%", (double)pos * 100 / sz); this->Update(); }); } } ~LoadingItem() { if (Iter) Iter->SetProgressFn(NULL); } bool SetText(const char *str, int i) { s = str; Update(); return true; } void OnPaint(LItem::ItemPaintCtx &Ctx) { LString msg; auto loading = LLoadString(IDS_LOADING); if (s) msg.Printf("%s %s", loading, s.Get()); else msg = loading; LDisplayString d(LSysFont, msg); LSysFont->Colour(L_TEXT, L_WORKSPACE); LSysFont->Transparent(false); d.Draw(Ctx.pDC, (Ctx.X()-d.X())/2, Ctx.y1, &Ctx); } void Select(bool b) { } }; bool ScribeFolder::UnloadThings() { bool Status = true; if (IsLoaded()) { // LgiTrace("Unloading %s, Items=%i\n", GetPath(), Items.Length()); // Emptying the item list, leave the store nodes around though Thing *t; while ((t = Items[0])) { if (t->GetDirty()) { t->Save(0); } t->SetObject(NULL, false, _FL); t->DecRef(); } Items.Empty(); IsLoaded(false); GetObject()->SetInt(FIELD_LOADED, false); } return Status; } #define DEBUG_PROF_LOAD_THINGS 0 #if DEBUG_PROF_LOAD_THINGS #define PROFILE(str) prof.Add("Add"); .Add(str) #else #define PROFILE(str) #endif Store3Status ScribeFolder::LoadThings(LViewI *Parent, std::function Callback) { int OldUnRead = GetUnRead(); auto FldObj = GetFldObj(); if (!FldObj) { LgiTrace("%s:%i - No folder object.\n", _FL); return Store3Error; } if (!Parent) Parent = App; auto ContinueLoading = [&]() { WhenLoaded(_FL, [this, OldUnRead, Callback]() { // This is called when all the Store3 objects are loaded int Unread = OldUnRead; if (Unread < 0) Unread = GetUnRead(); Loading.Reset(); auto &Children = GetFldObj()->Children(); if (Children.GetState() != Store3Loaded) { LAssert(!"Really should be loaded by now."); return; } for (auto c = Children.First(); c; c = Children.Next()) { auto t = CastThing(c); if (t) { // LAssert(Items.HasItem(t)); } else if ((t = App->CreateThingOfType((Store3ItemTypes) c->Type(), c))) { t->SetObject(c, false, _FL); t->SetParentFolder(this); t->OnSerialize(false); } } int NewUnRead = 0; for (auto t: Items) { if (t->GetFolder() != this) { #ifdef _DEBUG char s[256]; sprintf_s(s, sizeof(s), "%s:%i - Error, thing not parented correctly: this='%s', child='%x'\n", _FL, GetText(0), t->GetObject() ? t->GetObject()->Type() : 0); printf("%s", s); LgiMsg(App, s, AppName); #endif t->SetFolder(this); } Mail *m = t->IsMail(); if (m) NewUnRead += (m->GetFlags() & MAIL_READ) ? 0 : 1; t->SetFieldArray(FieldArray); } if (Unread != NewUnRead) OnUpdateUnRead(NewUnRead - Unread, false); Update(); if (d->IsInbox < 0 && App) { d->IsInbox = App->GetFolder(FOLDER_INBOX) == this; if (d->IsInbox > 0) UpdateOsUnread(); } if (Callback) Callback(Store3Success); }, 0); if (!IsLoaded()) { bool Ui = Tree ? Tree->InThread() : false; if (Ui) Tree->Capture(false); auto &Children = FldObj->Children(); auto Status = Children.GetState(); if (Status != Store3Loaded) { if (View() && Loading.Reset(Ui ? new LoadingItem(&Children) : NULL)) View()->Insert(Loading); return Status; // Ie deferred or error... } IsLoaded(true); } return Store3Loaded; }; auto Path = GetPath(); if (!App || !Path) { LAssert(!"We should probably always have an 'App' and 'Path' ptrs..."); return Store3Error; } std::function AccessCb; if (Callback) { AccessCb = [&](bool Access) { if (Access) ContinueLoading(); else Callback(Store3Error); }; } auto Access = App->GetAccessLevel( Parent, GetReadAccess(), Path, AccessCb); if (Access == Store3Error) { // No read access: LgiTrace("%s:%i - Folder read access denied.\n", _FL); // Emptying the item list, leave the store nodes around though for (auto t: Items) t->SetObject(NULL, false, _FL); Items.Empty(); IsLoaded(false); Update(); } else if (Access == Store3Success) { ContinueLoading(); } return Access; } void ScribeFolder::OnRename(char *NewName) { if (!NewName) return; int FolderType = App->GetFolderType(this); SetName(NewName, true); // Calls update too.. SetDirty(); if (FolderType >= 0) { // it's a system folder so reflect the changes in // the system folder tracking options. char KeyName[32]; sprintf_s(KeyName, sizeof(KeyName), "Folder-%i", FolderType); auto Path = GetPath(); if (Path) { LVariant v = Path.Get(); App->GetOptions()->SetValue(KeyName, v); } } } void ScribeFolder::OnDelete() { if (GetObject()) { char Msg[256]; sprintf_s(Msg, sizeof(Msg), LLoadString(IDS_DELETE_FOLDER_DLG), GetText()); int Result = LgiMsg(App, Msg, AppName, MB_YESNO); if (Result == IDYES) { if (App->GetMailList()) App->GetMailList()->RemoveAll(); ScribeFolder *Parent = GetFolder(); LArray Del; Del.Add(GetObject()); if (GetObject()->GetStore()->Delete(Del, true)) { if (Parent) Parent->Select(true); } } } } ScribeFolder *ScribeFolder::GetSubFolder(const char *Path) { ScribeFolder *Folder = 0; if (Path) { if (*Path == '/') Path++; char Name[256]; char *Sep = strchr((char*)Path, '/'); if (Sep) { ZeroObj(Name); memcpy(Name, Path, Sep-Path); } else { strcpy_s(Name, sizeof(Name), Path); } LTreeItem *Starts[2] = { GetChild(), IsRoot() ? GetNext() : 0 }; for (int s=0; !Folder && sGetNext()) { ScribeFolder *f = dynamic_cast(i); if (f) { auto n = f->GetName(true); if (n.Equals(Name)) { if (Sep) Folder = f->GetSubFolder(Sep+1); else Folder = f; break; } } } } } return Folder; } bool ScribeFolder::ReindexField(int OldIndex, int NewIndex) { if (!GetFldObj()) return false; auto &Flds = GetFldObj()->Fields(); if (GetFldObj()->Fields().Length() <= 0) return false; LDataPropI *f = Flds[OldIndex]; if (!f) return false; Flds.Delete(f); Flds.Insert(f, OldIndex < NewIndex ? NewIndex - 1 : NewIndex); int i=0; FieldArray.Length(0); for (f = Flds.First(); f; f = Flds.Next()) { auto Id = f->GetInt(FIELD_ID); FieldArray[i++] = (int)Id; } for (auto t: Items) t->SetFieldArray(FieldArray); SetDirty(); return true; } int ScribeFolder::_UnreadChildren() { int Status = 0; for (ScribeFolder *f=GetChildFolder(); f; f=f->GetNextFolder()) { int u = f->GetUnRead(); if (u > 0) Status += u; Status += f->_UnreadChildren(); } return Status; } void ScribeFolder::_PourText(LPoint &Size) { if (!d) return; const char *Text = GetText(); Size.x = Size.y = 0; if (Text && !d->DsBase) { if (GetUnRead() > 0 || ChildUnRead) { const char *StartCount = strrchr(Text, '('); ssize_t NameLen = StartCount ? StartCount-Text : strlen(Text); LFont *b = (App->GetBoldFont()) ? App->GetBoldFont() : GetTree()->GetFont(); d->DsBase.Reset(new LDisplayString(b, Text, NameLen)); d->DsUnread.Reset(new LDisplayString(GetTree()->GetFont(), StartCount)); } else { d->DsBase.Reset(new LDisplayString(GetTree()->GetFont(), Text)); } } if (d->DsBase) Size.x += d->DsBase->X(); if (d->DsUnread) Size.x += d->DsUnread->X(); Size.x += 4; Size.y = MAX(16, LSysFont->GetHeight()); } void ScribeFolder::_PaintText(LItem::ItemPaintCtx &Ctx) { if (!d) return; LRect *_Text = _GetRect(TreeItemText); if (d->DsBase) { LFont *f = d->DsBase->GetFont(); int Tab = f->TabSize(); f->TabSize(0); f->Transparent(false); f->Colour(Ctx.Fore, Ctx.TxtBack); d->DsBase->Draw(Ctx.pDC, _Text->x1 + 2, _Text->y1 + 1, _Text); if (d->DsUnread) { f = d->DsUnread->GetFont(); f->Transparent(true); LColour UnreadCol(App->GetColour(L_UNREAD_COUNT)); if ( abs ( (UnreadCol.GetGray() - Ctx.Back.GetGray()) ) < 80 ) { // too close.. use fore f->Colour(Ctx.Fore, Ctx.TxtBack); } else { // contrast ok.. use unread f->Colour(UnreadCol, Ctx.TxtBack); } d->DsUnread->Draw(Ctx.pDC, _Text->x1 + 2 + d->DsBase->X(), _Text->y1 + 1); } f->TabSize(Tab); if (Ctx.x2 > _Text->x2) { Ctx.pDC->Colour(Ctx.Back); Ctx.pDC->Rectangle(_Text->x2 + 1, Ctx.y1, Ctx.x2, Ctx.y2); } } else { Ctx.pDC->Colour(Ctx.Back); Ctx.pDC->Rectangle(&Ctx); } } bool ScribeFolder::Save(ScribeFolder *Into) { bool Status = false; if (GetObject()) { // saving a disk object Store3Status s = GetObject()->Save(); if (s != Store3Error) { Status = true; SetDirty(false); } else { LAssert(0); } } return Status; } void ScribeFolder::OnExpand(bool b) { if (b && LoadOnDemand) { DeleteObj(LoadOnDemand); if (App->ScribeState != ScribeWnd::ScribeExiting) { ScribeWnd::AppState Prev = App->ScribeState; App->ScribeState = ScribeWnd::ScribeLoadingFolders; LoadFolders(); if (App->ScribeState == ScribeWnd::ScribeExiting) { LCloseApp(); return; } App->ScribeState = Prev; } } OnUpdateUnRead(0, false); SetDirty(((uchar)b) != GetOpen()); SetOpen(b); } bool ScribeFolder::OnKey(LKey &k) { #ifndef WINDOWS // This is being done by the VK_APPS key on windows... if (k.IsContextMenu()) { if (k.Down()) { LMouse m; m.x = 5; m.y = 5; m.ViewCoords = true; m.Target = GetTree(); DoContextMenu(m); } return true; } #endif return false; } #define DefField(id, wid) \ { \ LDataPropI *Fld = Fields.Create(GetFldObj()->GetStore()); \ if (Fld) \ { \ Fld->SetInt(FIELD_ID, id); \ Fld->SetInt(FIELD_WIDTH, wid); \ Fields.Insert(Fld); \ } \ } void ScribeFolder::SetDefaultFields(bool Force) { if (!GetFldObj()) return; if (Force || GetFldObj()->Fields().Length() <= 0) { LDataIterator &Fields = GetFldObj()->Fields(); Fields.DeleteObjects(); int FolderType = App ? App->GetFolderType(this) : -1; if (FolderType == FOLDER_OUTBOX || FolderType == FOLDER_SENT) { DefField(FIELD_PRIORITY, 10); DefField(FIELD_FLAGS, 15); DefField(FIELD_TO, 150); DefField(FIELD_SUBJECT, 250); DefField(FIELD_SIZE, 80); DefField(FIELD_DATE_SENT, 100); } else { switch (GetItemType()) { case MAGIC_MAIL: { DefField(FIELD_PRIORITY, 10); DefField(FIELD_FLAGS, 10); DefField(FIELD_FROM, 150); DefField(FIELD_SUBJECT, 250); DefField(FIELD_SIZE, 80); DefField(FIELD_DATE_SENT, 100); break; } case MAGIC_CONTACT: { DefField(FIELD_FIRST_NAME, 200); DefField(FIELD_LAST_NAME, 200); DefField(FIELD_EMAIL, 300); break; } case MAGIC_CALENDAR: { DefField(FIELD_CAL_SUBJECT, 200); DefField(FIELD_CAL_START_UTC, 150); DefField(FIELD_CAL_END_UTC, 150); break; } case MAGIC_FILTER: { for (int i=0; DefaultFilterFields[i]; i++) { DefField(DefaultFilterFields[i], i == 0 ? 200 : 100); } break; } case MAGIC_GROUP: { DefField(FIELD_GROUP_NAME, 300); break; } default: break; } } // set for save SetDirty(Fields.Length() > 0); } else { // already has fields, so don't interfer with them } } LString ScribeFolder::GetPath() { LString::Array p; ScribeFolder *f = this; while (f) { p.Add(f->GetName(true)); f = dynamic_cast(f->GetParent()); } LString dir = "/"; return dir + dir.Join(p.Reverse()); } void ScribeFolder::SetName(const char *Name, bool Encode) { if (Name) { d->DsBase.Reset(); d->DsUnread.Reset(); NameCache.Reset(); if (GetObject()) GetObject()->SetStr(FIELD_FOLDER_NAME, Name); SetText(Name); // Calls update } } LString ScribeFolder::GetName(bool Decode) { if (!GetObject()) return NULL; auto In = GetObject()->GetStr(FIELD_FOLDER_NAME); if (!In) return NULL; if (Decode) return LUrlDecode(In); else return In; } void ScribeFolder::Update() { if (Tree && !Tree->InThread()) { // LgiTrace("%s:%i - Update not in thread?\n", _FL); return; } d->DsBase.Reset(); d->DsUnread.Reset(); NameCache.Reset(); LTreeItem::Update(); } const char *ScribeFolder::GetText(int i) { if (!NameCache) { LString Name = GetName(true); if (!Name) return "...loading..."; size_t NameLen = strlen(Name) + 40; NameCache.Reset(new char[NameLen]); if (NameCache) { bool ShowTotals = false; LVariant v; if (App->GetOptions()->GetValue(OPT_ShowFolderTotals, v)) ShowTotals = v.CastInt32() != 0; int c = sprintf_s(NameCache, NameLen, "%s", Name.Get()); auto ItemType = GetItemType(); if (ItemType && (ShowTotals || GetUnRead() || ChildUnRead)) { c += sprintf_s(NameCache+c, NameLen-c, " ("); if (ItemType == MAGIC_MAIL || ItemType == MAGIC_ANY) { const char *Ch = ChildUnRead ? "..." : ""; if (ShowTotals) c += sprintf_s(NameCache+c, NameLen-c, "%i%s/", GetUnRead(), Ch); else if (GetUnRead()) c += sprintf_s(NameCache+c, NameLen-c, "%i%s", GetUnRead(), Ch); else if (ChildUnRead) c += sprintf_s(NameCache+c, NameLen-c, "..."); } if (ShowTotals) { if (IsLoaded()) c += sprintf_s(NameCache+c, NameLen-c, "%zi", Items.Length()); else c += sprintf_s(NameCache+c, NameLen-c, "?"); } c += sprintf_s(NameCache+c, NameLen-c, ")"); } } } return NameCache; } int ScribeFolder::GetImage(int Flags) { if (GetItemType() == MAGIC_ANY) { return ICON_TRASH; } if (Flags) { return ICON_OPEN_FOLDER; } switch (GetItemType()) { case MAGIC_MAIL: return ICON_FOLDER_MAIL; case MAGIC_CONTACT: return ICON_FOLDER_CONTACTS; case MAGIC_FILTER: return ICON_FOLDER_FILTERS; case MAGIC_CALENDAR: return ICON_FOLDER_CALENDAR; case MAGIC_NONE: return ICON_MAILBOX; default: return ICON_CLOSED_FOLDER; } } int ScribeFolder::Sizeof() { return 0; } bool ScribeFolder::Serialize(LFile &f, bool Write) { return false; } class NullMail : public Mail { Thing &operator =(Thing &c) override { return *this; } public: NullMail(ScribeWnd *App, MContainer *c, ScribeFolder *f) : Mail(App) { SetFlags(MAIL_READ | MAIL_CREATED); Container = c; SetParentFolder(f); SetSubject((char*)LLoadString(IDS_MISSING_PARENT)); } ~NullMail() { SetParentFolder(NULL); } bool IsPlaceHolder() override { return true; } const char *GetText(int i) override { // Clear all the fields return 0; } int Compare(LListItem *Arg, ssize_t Field) override { // Use the first mail to sort into position if (Container) { MContainer *c = Container->Children.Length() ? Container->Children[0] : 0; if (c && c->Message) { return c->Message->Compare(Arg, Field); } } return -1; } void OnMouseClick(LMouse &m) override { // Disable the mouse click } bool SetDirty(bool b = true) override { // Don't set it to dirty, otherwise the dirty object // clean up code will save it into the outbox. return true; } }; template int TrashCompare(T *pa, T *pb, NativeInt Data) { ScribeFolder *f = (ScribeFolder*)Data; Thing *a = dynamic_cast(pa); Thing *b = dynamic_cast(pb); if (!a || !b) return 0; int type = a->Type() - b->Type(); if (type) return type; int col = f->GetSortCol(); int *defs = a->GetDefaultFields(); if (!defs || !defs[col]) return 0; return (f->GetSortAscend() ? 1 : -1) * a->Compare(b, defs[col]); } template int TrashCompare(LListItem *pa, LListItem *pb, NativeInt Data); int ListItemCompare(LListItem *a, LListItem *b, NativeInt Data) { ScribeFolder *f = (ScribeFolder*)Data; return (f->GetSortAscend() ? 1 : -1) * a->Compare(b, f->GetSortField()); } int ThingCompare(Thing *a, Thing *b, NativeInt Data) { ScribeFolder *f = (ScribeFolder*)Data; return (f->GetSortAscend() ? 1 : -1) * a->Compare(b, f->GetSortField()); } int ThingSorter(Thing *a, Thing *b, ThingSortParams *Params) { return (Params->SortAscend ? 1 : -1) * a->Compare(b, Params->SortField); } bool ScribeFolder::Thread() { bool Status = false; if (GetItemType() == MAGIC_MAIL) { int Thread = GetThreaded(); ThingList *Lst = View(); List m; for (auto t: Items) { Mail *e = t->IsMail(); if (e) { if (Thread) { m.Insert(e); } else if (e->Container) { DeleteObj(e->Container); } } } if (Lst && m[0]) { // Thread LArray Containers; MContainer::Thread(m, Containers); LAssert(m.Length() == Items.Length()); // Insert blank items for missing thread parents for (unsigned i=0; iMessage) { LAssert(c->Children.Length() > 1); c->Message = new NullMail(App, c, this); if (c->Message) { Lst->PlaceHolders.Insert(c->Message); if (View() && c->Message) { c->Message->SetFieldArray(FieldArray); Items.Insert(c->Message); } } } } // Sort root list LArray Containers2 = Containers; ThingSortParams Params; Params.SortAscend = GetSortAscend(); Params.SortField = GetSortField(); #if 0 for (int i=0; iMessage ? Containers[i]->Message->GetFieldText(Params.SortField) : NULL; LgiTrace("%p(%s)\n", Containers[i], Str1); } Containers.Sort(ContainerCompare); LgiQuickSort(Base, Containers2.Length(), ContainerSorter, &Params); for (int i=0; iMessage ? Containers[i]->Message->GetFieldText(Params.SortField) : NULL; char *Str2 = Containers2[i]->Message ? Containers2[i]->Message->GetFieldText(Params.SortField) : NULL; LgiTrace("%p(%s), %p(%s) - %s\n", Containers[i], Str1, Containers2[i], Str2, Containers[i]!=Containers2[i]?"DIFFERENT":""); } #else MContainer **Base = &Containers[0]; if (Containers.Length() > 1) LgiQuickSort(Base, Containers.Length(), ContainerSorter, &Params); /* for (int i=0; iMessage ? Containers[i]->Message->GetFieldText(Params.SortField) : NULL; LgiTrace("%p(%s)\n", Containers[i]->Message, Str1); } LgiTrace("\n"); */ #endif // Position and index the thread tree int Index = 0; for (unsigned i=0; iPour(Index, 0, 0, i < Containers.Length()-1, &Params); } // Sort all the items by index Items.Sort(ContainerIndexer); Status = true; /* int Idx = 0; for (Thing *t = Items.First(); t; t = Items.Next(), Idx++) { Mail *m = t->IsMail(); if (m) { char *Str = m->GetFieldText(Params.SortField); LgiTrace("%i,%i %p %s\n", Idx, m->Container ? m->Container->Index : -1, m, Str); } } */ } } else { for (auto t: Items) { Mail *e = t->IsMail(); if (e) DeleteObj(e->Container); } } return Status; } struct SortPairInt { Thing *t; uint64 ts; void SetDate(Thing *th, int sf) { t = th; auto obj = th->GetObject(); // Store3State loaded = (Store3State)obj->GetInt(FIELD_LOADED); auto dt = obj->GetDate(sf); ts = dt && dt->Year() ? dt->Ts() : 0; } void SetInt(Thing *th, int sf) { t = th; auto obj = th->GetObject(); // Store3State loaded = (Store3State)obj->GetInt(FIELD_LOADED); ts = obj->GetInt(sf); } static int Compare(SortPairInt *a, SortPairInt *b) { int64_t diff = (int64_t)a->ts - (int64_t)b->ts; if (diff < 0) return -1; return diff > 0 ? 1 : 0; } }; struct SortPairStr { Thing *t; const char *ts; void SetStr(Thing *th, int sf) { t = th; ts = th->GetObject()->GetStr(sf); } static int Compare(SortPairStr *a, SortPairStr *b) { return stricmp(a->ts ? a->ts : "", b->ts ? b->ts : ""); } }; bool ScribeFolder::SortItems() { int sf = GetSortField(); LVariantType type = GV_NULL; auto StartTs = LCurrentTime(); const static int TimeOut = 2/*sec*/ * 1000; switch (sf) { case FIELD_DATE_SENT: case FIELD_DATE_RECEIVED: type = GV_DATETIME; break; case FIELD_SUBJECT: type = GV_STRING; break; default: return false; } LArray intPairs; LArray strPairs; bool intType = type == GV_DATETIME || type == GV_INT32 || type == GV_INT64; if (intType) { intPairs.Length(Items.Length()); int n = 0; auto s = intPairs.AddressOf(); if (type == GV_DATETIME) { for (auto i: Items) { s[n++].SetDate(i, sf); if (n % 50 == 0 && LCurrentTime() - StartTs >= TimeOut) return false; } } else { for (auto i: Items) { s[n++].SetInt(i, sf); if (n % 50 == 0 && LCurrentTime() - StartTs >= TimeOut) return false; } } intPairs.Sort(SortPairInt::Compare); } else if (type == GV_STRING) { strPairs.Length(Items.Length()); int n = 0; auto s = strPairs.AddressOf(); for (auto i: Items) s[n++].SetStr(i, sf); strPairs.Sort(SortPairStr::Compare); } Items.Empty(); if (intType) { if (GetSortAscend()) for (auto i: intPairs) Items.Add(i.t); else for (auto it = intPairs.rbegin(); it != intPairs.end(); it--) Items.Add((*it).t); } else { if (GetSortAscend()) for (auto i: strPairs) Items.Add(i.t); else for (auto it = strPairs.rbegin(); it != strPairs.end(); it--) Items.Add((*it).t); } return true; } void ScribeFolder::Populate(ThingList *list) { LProfile Prof("ScribeFolder::Populate", 1000); App->OnSelect(); if (!GetFldObj() || !list) return; CurState = FldState_Populating; ScribeFolder *Prev = list->GetContainer(); bool Refresh = Prev == this; // Remove old items from list Prof.Add("Delete Placeholders"); list->DeletePlaceHolders(); list->RemoveAll(); if (!Refresh || list->GetColumns() == 0 || GetItemType() == MAGIC_FILTER) { if (Prev) { // save previous folders settings Prev->SerializeFieldWidths(); } Prof.Add("Empty cols"); LVariant GridLines; if (App->GetOptions()->GetValue(OPT_GridLines, GridLines)) { list->DrawGridLines(GridLines.CastInt32() != 0); } list->EmptyColumns(); Prof.Add("Set def fields"); bool ForceDefaultFields = GetItemType() == MAGIC_FILTER; if (GetFldObj()->Fields().Length() <= 0 || ForceDefaultFields) { SetDefaultFields(ForceDefaultFields); } // Add fields to list view int n = 0; LArray Empty; for (auto t: Items) { t->SetFieldArray(Empty); } LRect *Bounds = 0; if (App->GetIconImgList()) { Bounds = App->GetIconImgList()->GetBounds(); } switch (GetItemType()) { case MAGIC_ANY: { list->AddColumn("", 170); list->AddColumn("", 170); list->AddColumn("", 170); list->AddColumn("", 170); break; } default: { n = 0; FieldArray.Length(0); for (LDataPropI *i = GetFldObj()->Fields().First(); i; i = GetFldObj()->Fields().Next()) { int FieldId = (int)i->GetInt(FIELD_ID); const char *FName = LLoadString(FieldId); int Width = (int)i->GetInt(FIELD_WIDTH); const char *FieldText = FName ? FName : i->GetStr(FIELD_NAME); LAssert(FieldText != NULL); LItemColumn *c = list->AddColumn(FieldText, Width); if (c) { switch (i->GetInt(FIELD_ID)) { case FIELD_PRIORITY: { int x = 12; if (Bounds) { x = Bounds[ICON_PRIORITY_BLACK].X() + 7; } c->Width(x); c->Image(ICON_PRIORITY_BLACK); c->Resizable(false); break; } case FIELD_FLAGS: { int x = 14; if (Bounds) { x = Bounds[ICON_FLAGS_BLACK].X() + 7; } c->Width(x); c->Image(ICON_FLAGS_BLACK); c->Resizable(false); break; } case FIELD_SIZE: { c->TextAlign(LCss::Len(LCss::AlignRight)); break; } } } FieldArray[n++] = (int)i->GetInt(FIELD_ID); } break; } } // Add all items to list if (View()) { View()->SetContainer(this); // tell the list who we are if (GetSortCol() >= 0) { // set current sort settings View()->SetSort(GetSortCol(), GetSortAscend()); } } Prof.Add("Load things"); // FIXME: LoadThings(); } // Filter List Is; // Do any threading/sorting LString SortMsg; if (!Thread() && GetSortField()) { SortMsg.Printf("Sorting " LPrintfInt64 " items", Items.Length()); Prof.Add(SortMsg); if (GetItemType() == MAGIC_ANY) { Items.Sort(TrashCompare, (NativeInt)this); } else { // Sort.. // if (!SortItems()) Items.Sort(ThingCompare, (NativeInt)this); } } // Do any filtering... Prof.Add("Filtering"); ThingFilter *Filter = App->GetThingFilter(); auto FilterStart = LCurrentTime(); size_t Pos = 0; for (auto t: Items) { t->SetFieldArray(FieldArray); if (!Filter || Filter->TestThing(t)) { // Add anyway... because all items are not part of list Is.Insert(t); } if ((LCurrentTime()-FilterStart) > 300) { if (!Loading && Loading.Reset(new LoadingItem(NULL))) list->Insert(Loading, 0); if (Loading) { LString s; s.Printf(LPrintfInt64 " of " LPrintfInt64 ", %.1f%%", Pos, Items.Length(), (double)Pos * 100 / Items.Length()); Loading->SetText(s); } FilterStart = LCurrentTime(); } Pos++; } Prof.Add("Inserting"); if (View() && Is[0]) { View()->Insert(Is, -1, true); } Prof.Add("Deleting"); list->DeletePlaceHolders(); Loading.Reset(); Prof.Add("OnSelect"); GetFldObj()->OnSelect(true); CurState = FldState_Idle; } void ScribeFolder::OnUpdateUnRead(int Offset, bool ScanItems) { if (!d->InUpdateUnread) { d->InUpdateUnread = true; int OldUnRead = GetUnRead(); d->DsBase.Reset(); d->DsUnread.Reset(); NameCache.Reset(); if (ScanItems) { if (GetItemType() == MAGIC_MAIL || GetItemType() == MAGIC_ANY) { size_t Count = 0; for (auto t: Items) { Mail *m = t->IsMail(); if (m && !TestFlag(m->GetFlags(), MAIL_READ)) Count++; } SetUnRead((int32_t)Count); } } else if (Offset != 0) { SetUnRead(GetUnRead() + Offset); } if (GetUnRead() < 0) SetUnRead(0); if (OldUnRead != GetUnRead()) { for (LTreeItem *i = GetParent(); i; i = i->GetParent()) { ScribeFolder *tc = dynamic_cast(i); if (tc && tc->GetParent()) tc->OnUpdateUnRead(0, false); } SetDirty(); if (d->IsInbox > 0) UpdateOsUnread(); } ChildUnRead = Expanded() ? 0 : _UnreadChildren(); Update(); d->InUpdateUnread = false; } } void ScribeFolder::EmptyFieldList() { if (GetFldObj()) GetFldObj()->Fields().DeleteObjects(); } void ScribeFolder::SerializeFieldWidths(bool Write) { if (GetFldObj() && View()) { // LAssert(View()->GetColumns() == Object->Fields().Length()); int Cols = MIN(View()->GetColumns(), (int)GetFldObj()->Fields().Length()); for (int i=0; iColumnAt(i); LDataPropI *f = GetFldObj()->Fields()[i]; if (c && f) { if (f->GetInt(FIELD_WIDTH) != c->Width()) { if (Write) { c->Width((int)f->GetInt(FIELD_WIDTH)); } else { f->SetInt(FIELD_WIDTH, c->Width()); SetDirty(); } } } else LAssert(0); } } } void ScribeFolder::OnProperties(int Tab) { if (!GetObject()) return; SerializeFieldWidths(); if (View()) { SetSort(View()->GetSortCol(), View()->GetSortAscending()); } OpenFolderProperties(this, Tab, [this](auto repop) { if (repop) { SetDirty(); SerializeFieldWidths(true); Populate(View()); } }); } ScribeFolder *ScribeFolder::CreateSubDirectory(const char *Name, int Type) { ScribeFolder *NewFolder = 0; auto ThisObj = dynamic_cast(GetObject()); if (Name && ThisObj && ThisObj->GetStore()) { LDataI *Fld = GetObject()->GetStore()->Create(MAGIC_FOLDER); if (Fld) { LDataFolderI *Obj = dynamic_cast(Fld); if (Obj) { NewFolder = new ScribeFolder; if (NewFolder) { NewFolder->App = App; NewFolder->SetObject(Obj, false, _FL); // Set name and type NewFolder->SetName(Name, true); NewFolder->GetObject()->SetInt(FIELD_FOLDER_TYPE, Type); ThisObj->SubFolders(); if (NewFolder->GetObject()->Save(ThisObj)) { Insert(NewFolder); NewFolder->SetDefaultFields(); NewFolder->OnItemType(); } else { DeleteObj(NewFolder); } } } } } return NewFolder; } bool ScribeFolder::Delete(LArray &Items, bool ToTrash) { if (!App) return false; List NotNew; LArray Del; LDataStoreI *Store = NULL; for (auto i: Items) { if (i->IsPlaceHolder()) { i->DecRef(); } else { Mail *m = i->IsMail(); if (m) NotNew.Insert(m); auto ObjStore = i->GetObject() ? i->GetObject()->GetStore() : NULL; if (!Store) Store = ObjStore; if (Store == ObjStore) Del.Add(i->GetObject()); else LAssert(!"All objects must have the same store."); } } if (NotNew.Length()) App->OnNewMail(&NotNew, false); if (Del.Length() == 0) return true; if (!Store) { LgiTrace("%s:%i - No Store?\n", _FL); return false; } if (!Store->Delete(Del, ToTrash)) { LgiTrace("%s:%i - Store.Delete failed.\n", _FL); return false; } return true; } #define MoveToStatus(idx, status) if (Status) { (*Status)[idx] = (status); } bool ScribeFolder::MoveTo(LArray &Items, bool CopyOnly, LArray *Status) { if (Items.Length() == 0) return false; if (!GetObject() || !App) return false; auto FolderItemType = GetItemType(); for (unsigned i=0; iGetObject()) { MoveToStatus(i, Store3Error); } else { auto ThingItemType = t->Type(); if (!(FolderItemType == ThingItemType || FolderItemType == MAGIC_ANY)) MoveToStatus(i, Store3Error); } } LVariant BayesInc; if (App) App->GetOptions()->GetValue(OPT_BayesIncremental, BayesInc); auto ThisFolderPath = GetPath(); auto NewBayesType = App->BayesTypeFromPath(ThisFolderPath); int NewFolderType = App->GetFolderType(this); ScribeFolder *TemplatesFolder = App->GetFolder(FOLDER_TEMPLATES); bool BuildDynMenus = this == TemplatesFolder; LArray InStoreMove; // Object in the same store... auto ThisStore = GetObject()->GetStore(); LHashTbl, bool> Map; for (unsigned i=0; iGetFolder(); LString Path; if (Old) { Path = Old->GetPath(); if (Old == TemplatesFolder) // Moving to or from the templates folder... update the menu BuildDynMenus = true; } ScribeMailType OldBayesType = BayesMailUnknown; if (BayesInc.CastInt32() && t->IsMail() && TestFlag(t->IsMail()->GetFlags(), MAIL_READ)) { OldBayesType = App->BayesTypeFromPath(t->IsMail()); } auto DoMove = [&]() { int OldFolderType = Old ? App->GetFolderType(Old) : -1; if ( (OldFolderType == FOLDER_TRASH || OldFolderType == FOLDER_SENT) && NewFolderType == FOLDER_TRASH) { // Delete for good auto Success = Old ? Old->DeleteThing(t, NULL) : Store3Error; MoveToStatus(i, Success ? Store3Success : Store3Error); if (Success) t->OnMove(); } else { // If this folder is currently selected... if (Select()) { // Insert item into list t->SetFieldArray(FieldArray); } if (CopyOnly) { LDataI *NewT = ThisStore->Create(t->Type()); if (NewT) { NewT->CopyProps(*t->GetObject()); auto s = NewT->Save(GetObject()); MoveToStatus(i, s); } else { MoveToStatus(i, Store3Error); } } else { if (NewFolderType != FOLDER_TRASH && OldBayesType != NewBayesType) { App->OnBayesianMailEvent(t->IsMail(), OldBayesType, NewBayesType); } // Move to this folder auto o = t->GetObject(); if (o && o->GetStore() == ThisStore) { InStoreMove.Add(o); Map.Add(i, true); } else { // Out of store more... use the old single object method... for the moment.. Store3Status s = t->SetFolder(this); MoveToStatus(i, s); if (s == Store3Success) { // Remove from the list.. if (Old && Old->Select() && App->MailList) App->MailList->Remove(t); t->OnMove(); } else if (s == Store3Error) { LgiTrace("%s:%i - SetFolder failed.\n", _FL); } } } } }; if (App) { App->GetAccessLevel(App, Old->GetFolderPerms(ScribeWriteAccess), Path, [&](bool Allow) { if (Allow) DoMove(); else MoveToStatus(i, Store3NoPermissions); }); } else DoMove(); } // FIXME: This code that runs at the end needs to wait for the DoMove events to complete somehow... if (InStoreMove.Length()) { auto Fld = dynamic_cast(GetObject()); if (!Fld) return false; auto s = ThisStore->Move(Fld, InStoreMove); for (unsigned i=0; iGetFolder() == this); LAssert(Items.HasItem(t)); } } } if (s <= Store3Error) return false; } if (BuildDynMenus) // Moving to or from the templates folder... update the menu App->BuildDynMenus(); return true; } int ThingFilterCompare(Thing *a, Thing *b, NativeInt Data) { Filter *A = a->IsFilter(); Filter *B = b->IsFilter(); return (A && B) ? A->GetIndex() - B->GetIndex() : 0; } void ScribeFolder::ReSort() { if (View() && Select()) { View()->SetSort(GetSortCol(), GetSortAscend()); } } void ScribeFolder::SetSort(int Col, bool Ascend, bool CanDirty) { if (GetItemType() == MAGIC_FILTER) { // Remove any holes in the indexing int i = 1; Items.Sort(ThingFilterCompare); for (auto t : Items) { Filter *f = t->IsFilter(); if (f) { if (f->GetIndex() != i) { f->SetIndex(i); } i++; } } } if (GetSortCol() != Col || GetSortAscend() != (uchar)Ascend) { GetObject()->SetInt(FIELD_SORT, (Col + 1) * (Ascend ? 1 : -1)); if (CanDirty) SetDirty(); } } int ScribeFolder::GetSortField() { int Status = 0; int Col = GetSortCol(); if (Col >= 0 && Col < (int)FieldArray.Length()) Status = FieldArray[Col]; return Status; } class FolderStream : public LStream { ScribeFolder *f; // Current index into the thing array int Idx; // Total known size of stream... int64 Sz; // A memory buffer containing just the encoded 'Thing' LMemStream Mem; LString FolderMime; public: FolderStream(ScribeFolder *folder) : Mem(512 << 10) { f = folder; Idx = 0; Sz = 0; switch (f->GetItemType()) { case MAGIC_MAIL: FolderMime = sMimeMbox; break; case MAGIC_CONTACT: FolderMime = sMimeVCard; break; case MAGIC_CALENDAR: FolderMime = sMimeVCalendar; break; default: LAssert(!"Need a mime type?"); break; } } int GetIndex() { return Idx; } int Open(const char *Str = 0,int Int = 0) { return true; } bool IsOpen() { return true; } int Close() { return 0; } int64 GetSize() { return -1; } int64 SetSize(int64 Size) { return -1; } int64 GetPos() { return -1; } int64 SetPos(int64 pos) { // This means that IStream::Seek return E_NOTIMPL, which is important // for windows to support copies of unknown size. return -1; } ssize_t Read(void *Buffer, ssize_t Size, int Flags = 0) { // Read from stream.. int64 Remaining = Mem.GetSize() - Mem.GetPos(); if (Remaining <= 0) { Thing *t = Idx < (ssize_t)f->Items.Length() ? f->Items[Idx++] : NULL; if (t) { Mem.SetSize(0); // We can't allow Export to delete the Mem object, we own it. // So create a proxy object for it. LAutoPtr cp(new LProxyStream(&Mem)); if (t->Export(cp, FolderMime)) Sz += Mem.GetSize(); else LAssert(0); Mem.SetPos(0); } else return 0; } Remaining = Mem.GetSize() - Mem.GetPos(); if (Remaining > 0) { int Common = (int)MIN(Size, Remaining); ssize_t Rd = Mem.Read(Buffer, Common); if (Rd > 0) return Rd; else LAssert(0); } return 0; } ssize_t Write(const void *Buffer, ssize_t Size, int Flags = 0) { LAssert(0); return 0; } LStreamI *Clone() { LAssert(0); return NULL; } }; // ScribeFolder drag'n'drop bool ScribeFolder::GetFormats(LDragFormats &Formats) { if (GetItemType() == MAGIC_MAIL || GetItemType() == MAGIC_CONTACT || GetItemType() == MAGIC_CALENDAR) { Formats.SupportsFileStreams(); } Formats.Supports(ScribeFolderObject); return Formats.Length() > 0; } bool ScribeFolder::OnBeginDrag(LMouse &m) { if (GetParent()) Drag(Tree, m.Event, DROPEFFECT_MOVE | DROPEFFECT_COPY); return true; } void ScribeFolder::OnEndData() { DropFileName.Empty(); } LString ScribeFolder::GetDropFileName() { LAutoString Fn(MakeFileName(GetObject()->GetStr(FIELD_FOLDER_NAME), 0)); DropFileName = Fn; if (GetItemType() == MAGIC_MAIL) DropFileName += ".mbx"; else if (GetItemType() == MAGIC_CONTACT) DropFileName += ".vcf"; else if (GetItemType() == MAGIC_CALENDAR) DropFileName += ".ics"; else if (GetItemType() == MAGIC_FILTER) DropFileName += ".xml"; return DropFileName; } bool ScribeFolder::GetData(LArray &Data) { ssize_t DataSet = 0; for (unsigned idx=0; idxSetPos(0); auto Fn = GetDropFileName(); auto MimeType = sMimeMbox; auto r = dd.AddFileStream(LGetLeaf(Fn), MimeType, s); LAssert(r); } DataSet = dd.Data.Length(); } else if (dd.IsFormat(LGI_FileDropFormat)) { LMouse m; if (App->GetMouse(m, true)) { LString::Array Files; if (GetDropFileName()) { LAutoPtr f(new LFile); if (f->Open(DropFileName, O_WRITE)) { if (GetItemType() == MAGIC_MAIL) Export(AutoCast(f), sMimeMbox); else if (GetItemType() == MAGIC_CONTACT) Export(AutoCast(f), sMimeVCard); else if (GetItemType() == MAGIC_CALENDAR) Export(AutoCast(f), sMimeVCalendar); } } if (DropFileName) { Files.Add(DropFileName.Get()); if (CreateFileDrop(&dd, m, Files)) { DataSet++; } else LgiTrace("%s:%i - CreateFileDrop failed.\n", _FL); } else LgiTrace("%s:%i - No drop file name.\n", _FL); } else LgiTrace("%s:%i - GetMouse failed.\n", _FL); } else if (dd.IsFormat(ScribeFolderObject)) { ScribeClipboardFmt *Fmt = ScribeClipboardFmt::Alloc(true, 1); if (Fmt) { Fmt->FolderAt(0, this); dd.Data[0].SetBinary(Fmt->Sizeof(), Fmt); DataSet++; free(Fmt); } } } return DataSet > 0; } void ScribeFolder::CollectSubFolderMail(ScribeFolder *To) { if (!To) To = this; LoadThings(NULL, [&](auto Status) { LArray Items; for (auto Item: Items) { if (To != this && Item->IsMail()) Items.Add(Item); } To->MoveTo(Items); for (ScribeFolder *f = GetChildFolder(); f; f = f->GetNextFolder()) { f->CollectSubFolderMail(To); } }); } void ScribeFolder::OnReceiveFiles(LArray &Files) { if (Files.Length()) { for (unsigned i=0; i f(new LTextFile); if (f->Open(File, O_READ)) Import(AutoCast(f), MimeType); } } } // Import/export bool ScribeFolder::GetFormats(bool Export, LString::Array &MimeTypes) { MimeTypes.Add(sMimeMbox); if (!Export) MimeTypes.Add(sMimeMessage); return MimeTypes.Length() > 0; } class MboxParser : public LStringPipe { LStreamI *Src = NULL; int Hdrs = 0; bool NewMsg = true; LArray Buf; int64 Pos = 0; bool Eof = false; bool IsMessageHdr(LString c) { // check that it's a from line auto parts = c.SplitDelimit(" \r"); if (parts.Length() >= 7 && parts.Length() <= 9 && parts[0].Equals("From")) { return true; } return false; } struct Blk { uint8_t *ptr; ssize_t size; }; struct Blocks : public LArray { ssize_t Bytes = 0; Blocks(LMemQueue *q) { q->Iterate([this](auto ptr, auto size) { auto &b = New(); b.ptr = ptr; b.size = size; Bytes += size; return true; }); } LString GetLine(size_t idx, size_t offset) { char buf[256]; int ch = 0; for (size_t i = idx; i < Length(); i++) { auto &b = (*this)[i]; auto p = b.ptr + offset; auto end = b.ptr + b.size; while (p < end) { if (*p == '\n' || ch == sizeof(buf)-1) return LString(buf, ch); buf[ch++] = *p++; } } return LString(); } bool ValidateSeparator(LString ln) { auto p = ln.SplitDelimit(); if (p.Length() < 7) return false; if (!p[0].Equals("From")) return false; if (p[1].Find("@") < 0) return false; bool hasYear = false; bool hasTime = false; for (int i=2; i= 1800 && val < 2200) hasYear = true; } else if (s.Find(":") > 0) { int colons = 0, nonDigits = 0; for (auto p = s.Get(); *p; p++) if (*p == ':') colons++; else if (!IsDigit(*p)) nonDigits++; if (colons == 2 && nonDigits == 0) hasTime = true; } } return hasYear && hasTime; } ssize_t FindBoundary(ssize_t start) { const char *key = "\nFrom "; const char *k = key + 1; size_t idx = 0; ssize_t offset = 0; if (Bytes == 0) return -1; if (start < 0 || start >= Bytes) { LAssert(!"Start out of range."); return -1; } // Seek to the right starting block... while (idx < Length()) { auto &b = (*this)[idx]; if (start < b.size) break; start -= b.size; offset += b.size; idx++; } // Start searching for the key... while (idx < Length()) { auto &b = (*this)[idx]; auto end = b.ptr + b.size; for (auto p = b.ptr + start; p < end; p++) { if (*k == *p) { if (*++k == 0) { // Found the "From " part, but lets check the rest of the line. // Should be in the format: // From sender date more-info auto blkAddr = (p - b.ptr) - 4; LString ln = GetLine(idx, blkAddr); if (ln && ValidateSeparator(ln)) return offset + blkAddr; } } else k = key; } offset += b.size; idx++; } return -1; } LRange FindMsg(MboxParser &parser) { auto start = FindBoundary(0); if (start > 0) LgiTrace("%s:%i - Usually the start should be 0, but it's " LPrintfSSizeT "?\n", _FL, start); if (start >= 0) { auto end = FindBoundary(start + 5); if (end > start) { return LRange(start, end - start); } else if (parser.Eof) { return LRange(start, Bytes); } } return LRange(-1, 0); } }; public: MboxParser(LStreamI *s) : LStringPipe(128 << 10) { Src = s; // _debug = true; Buf.Length(128 << 10); } bool ReadSource() { auto rd = Src->Read(Buf.AddressOf(), Buf.Length()); if (rd <= 0) { // Src stream is empty or in an error state.. Eof = true; return false; } auto wr = Write(Buf.AddressOf(), Buf.Length()); if (wr <= 0) { LgiTrace("%s:%i - Failed to write to local buffer.\n", _FL); return false; } return true; } LAutoPtr ReadMessage() { LAutoPtr m; while (true) { Blocks blks(this); auto r = blks.FindMsg(*this); if (r.Start >= 0) { LMemStream *ms = NULL; /* LgiTrace("ReadMsg " LPrintfInt64 " %s\n", Pos, r.GetStr()); auto InitPos = Pos; */ Pos += r.Len; m.Reset(ms = new LMemStream(this, r.Start, r.Len)); /* Debugging... auto key = "The package name is vmware_addons."; auto base = ms->GetBasePtr(); auto result = Strnistr(base, key, m->GetSize()); if (result) LgiTrace("Found the Key @ " LPrintfInt64 "\n", InitPos + (result - base)); */ break; } else if (!ReadSource()) { r = blks.FindMsg(*this); if (r.Start >= 0) m.Reset(new LMemStream(this, r.Start, r.Len)); break; } } return m; } }; -class FolderTask : public LProgressDlg -{ -protected: - ScribeWnd *App = NULL; - ScribeFolder *Folder = NULL; - - LString MimeType; - - LAutoPtr Stream; - - ThingType::IoProgress Status; - ThingType::IoProgressCallback onComplete; - -public: - // Minimum amount of time to do work. - constexpr static int WORK_SLICE_MS = 130; - // This should be larger then WORK_SLICE_MS to allow message loop to process - constexpr static int PULSE_MS = 200; - - FolderTask( ScribeFolder *folder, - LAutoPtr stream, - LString mimeType, - ThingType::IoProgressCallback cb) : - LProgressDlg(folder->App), - Folder(folder), - Stream(stream), - MimeType(mimeType), - onComplete(cb), - Status(Store3Success) - { - App = Folder->App; - Ts = LCurrentTime(); - SetParent(Folder->GetTree()); - SetPulse(PULSE_MS); - SetAlwaysOnTop(true); - - App->OnFolderTask(this, true); - } - - virtual ~FolderTask() - { - Folder->App->OnFolderTask(this, false); - - if (onComplete) - onComplete(&Status, Stream); - } - - bool OnRequestClose(bool OsClose) - { - return true; - } - - void OnPulse() - { - LProgressDlg::OnPulse(); - - auto StartTs = LCurrentTime(); - while ( !IsCancelled() && - (LCurrentTime() - StartTs) < WORK_SLICE_MS) - { - if (!TimeSlice()) - { - Quit(); - break; - } - } - } - - /// This should use around WORK_SLICE_MS of time and then - /// \returns true if more work to do or false if finished. - virtual bool TimeSlice() = 0; -}; - class ImportFolderTask : public FolderTask { LDataStoreI::StoreTrans trans; LAutoPtr Parser; public: ImportFolderTask(ScribeFolder *fld, LAutoPtr in, LString mimeType, ThingType::IoProgressCallback cb) : FolderTask(fld, in, mimeType, cb) { SetDescription(LLoadString(IDS_MBOX_READING)); SetType("K"); SetScale(1.0/1024.0); SetRange(Stream->GetSize()); trans = fld->GetObject()->GetStore()->StartTransaction(); } bool TimeSlice() { auto Start = LCurrentTime(); bool Eof = false; if (!Parser) Parser.Reset(new MboxParser(Stream)); while ( Parser && LCurrentTime() - Start < WORK_SLICE_MS && !IsCancelled()) { auto Msg = Parser->ReadMessage(); if (Msg) { Mail *m = dynamic_cast(App->CreateItem(MAGIC_MAIL, Folder, false)); if (m) { m->OnAfterReceive(Msg); m->SetFlags(MAIL_RECEIVED|MAIL_READ); m->Save(); m->Update(); } else { Eof = true; break; } } else { Eof = true; break; } Value(Stream->GetPos()); } return !Eof; } }; ThingType::IoProgress ScribeFolder::Import(IoProgressImplArgs) { if (Stricmp(mimeType, sMimeMbox) == 0 || Stricmp(mimeType, "text/x-mail") == 0) { // Mail box format... ThingType::IoProgress p(Store3Delayed); p.prog = new ImportFolderTask(this, stream, mimeType, cb); return p; } else if (Stricmp(mimeType, sMimeVCard) == 0) { VCard Io; Thing *t; bool Error = false; int Imported = 0; int Idx = 0; while ((t = App->CreateItem(GetItemType(), 0, false))) { Contact *c = t->IsContact(); if (!c) { t->DecRef(); Error = true; break; } if (Io.Import(c->GetObject(), stream)) { const char *First = 0, *Last = 0; c->GetField(FIELD_FIRST_NAME, First); c->GetField(FIELD_LAST_NAME, Last); LgiTrace("Import %i %s %s\n", Idx, First, Last); if (t->Save(this)) { Imported++; } else { Error = true; break; } } else { t->DecRef(); break; } Idx++; } if (Error) { LgiMsg( App, LLoadString(IDS_ERROR_IMPORT_COUNT), AppName, MB_OK, LLoadString(IDC_CONTACTS), Imported); IoProgressError("Contact import error."); } IoProgressSuccess(); } else if (Stricmp(mimeType, sMimeVCalendar) == 0) { VCal Io; Thing *t; while ((t = App->CreateItem(GetItemType(), this, false))) { if (Io.Import(t->GetObject(), stream)) { if (!t->Save(this)) IoProgressError("Contact save failed."); } else { t->DecRef(); break; } } IoProgressSuccess(); } else if (GetObject()) { Thing *t = App->CreateThingOfType(GetItemType(), GetObject()->GetStore()->Create(GetItemType())); if (!t) IoProgressError("Failed to create contact"); if (!t->Import(stream, mimeType)) { t->DecRef(); IoProgressError("Contact import failed."); } if (!t->Save(this)) { t->DecRef(); IoProgressError("Contact save failed."); } IoProgressSuccess(); } IoProgressNotImpl(); } class ExportFolderTask : public FolderTask { int Idx = 0; public: ExportFolderTask( ScribeFolder *folder, LAutoPtr out, LString mimeType, ThingType::IoProgressCallback cb) : FolderTask(folder, out, mimeType, cb) { bool Mbox = _stricmp(MimeType, sMimeMbox) == 0; // Clear the files contents Stream->SetSize(0); // Setup progress UI SetDescription(Mbox ? LLoadString(IDS_MBOX_WRITING) : (char*)"Writing..."); SetRange(Folder->Items.Length()); switch (Folder->GetItemType()) { case MAGIC_MAIL: SetType(LLoadString(IDS_EMAIL)); break; case MAGIC_CALENDAR: SetType(LLoadString(IDS_CALENDAR)); break; case MAGIC_CONTACT: SetType(LLoadString(IDS_CONTACT)); break; case MAGIC_GROUP: SetType("Groups"); break; default: SetType("Objects"); break; } SetPulse(PULSE_MS); SetAlwaysOnTop(true); } bool TimeSlice() { auto Start = LCurrentTime(); while ( LCurrentTime() - Start < WORK_SLICE_MS && !IsCancelled()) { if (Idx >= (ssize_t)Folder->Items.Length()) return false; // Process all the container's items Thing *t = Folder->Items[Idx++]; if (!t) return false; LAutoPtr wrapper(new LProxyStream(Stream)); if (!t->Export(wrapper, MimeType)) { Status.status = Store3Error; Status.errMsg = "Error exporting items."; return false; } Value(Idx); } return true; } /* void OnPulse() { LProgressDlg::OnPulse(); PostEvent(M_EXPORT_NEXT); } LMessage::Result OnEvent(LMessage *Msg) { if (Msg->Msg() == M_EXPORT_NEXT) { auto StartTs = LCurrentTime(); while ( !IsCancelled() && (LCurrentTime() - StartTs) < WORK_SLICE_MS) { if (Idx >= (ssize_t)Folder->Items.Length()) { Quit(); break; } // Process all the container's items Thing *t = Folder->Items[Idx++]; if (t) { LAutoPtr wrapper(new LProxyStream(Out)); if (t->Export(wrapper, MimeType)) { Value(Idx); } else { Status.status = Store3Error; Status.errMsg = "Error exporting items."; if (!onComplete) LgiMsg(this, "%s", AppName, MB_OK, Status.errMsg.Get()); Quit(); } } } return 0; } return LProgressDlg::OnEvent(Msg); } */ }; class FolderExportTask : public LProgressDlg { LAutoPtr Out; ScribeFolder *Folder; LString MimeType; int Idx; bool HasError = false; public: // Minimum amount of time to do work. constexpr static int WORK_SLICE_MS = 130; // This should be larger then WORK_SLICE_MS to allow message loop to process constexpr static int PULSE_MS = 200; FolderExportTask(LAutoPtr out, ScribeFolder *folder, LString mimeType) : LProgressDlg(folder->App) { Out = out; Folder = folder; MimeType = mimeType; Idx = 0; Ts = LCurrentTime(); Folder->App->OnFolderTask(this, true); bool Mbox = _stricmp(MimeType, sMimeMbox) == 0; // Clear the files contents Out->SetSize(0); // Setup progress UI SetDescription(Mbox ? LLoadString(IDS_MBOX_WRITING) : (char*)"Writing..."); SetRange(Folder->Items.Length()); switch (Folder->GetItemType()) { case MAGIC_MAIL: SetType(LLoadString(IDS_EMAIL)); break; case MAGIC_CALENDAR: SetType(LLoadString(IDS_CALENDAR)); break; case MAGIC_CONTACT: SetType(LLoadString(IDS_CONTACT)); break; case MAGIC_GROUP: SetType("Groups"); break; default: SetType("Objects"); break; } SetPulse(PULSE_MS); SetAlwaysOnTop(true); } ~FolderExportTask() { Folder->App->OnFolderTask(this, false); } bool OnRequestClose(bool OsClose) { return true; } void OnPulse() { LProgressDlg::OnPulse(); PostEvent(M_EXPORT_NEXT); } LMessage::Result OnEvent(LMessage *Msg) { if (Msg->Msg() == M_EXPORT_NEXT) { auto StartTs = LCurrentTime(); while ( !IsCancelled() && (LCurrentTime() - StartTs) < WORK_SLICE_MS) { if (Idx >= (ssize_t)Folder->Items.Length()) { Quit(); break; } // Process all the container's items Thing *t = Folder->Items[Idx++]; if (t) { if (t->Export(Out, MimeType, NULL)) { Value(Idx); } else { HasError = true; LgiMsg(this, "Error exporting items.", AppName); Quit(); } } } return 0; } return LProgressDlg::OnEvent(Msg); } }; // This is the mime type used to storage objects on disk const char *ScribeFolder::GetStorageMimeType() { auto Type = GetItemType(); switch (Type) { case MAGIC_MAIL: return sMimeMbox; case MAGIC_CALENDAR: return sMimeVCalendar; case MAGIC_CONTACT: return sMimeVCard; case MAGIC_FILTER: return sMimeXml; default: LgiTrace("%s:%i - Unsupported storage type: %s\n", _FL, Store3ItemTypeName(Type)); break; } return NULL; } void ScribeFolder::ExportAsync(LAutoPtr f, const char *MimeType, std::function Callback) { if (!MimeType) { LAssert(!"No Mimetype"); if (Callback) Callback(NULL); return; } LoadThings(NULL, [&](auto Status) { FolderExportTask *Task = NULL; if (Status == Store3Success) Task = new FolderExportTask(f, this, MimeType); if (Callback) Callback(Task); }); } ThingType::IoProgress ScribeFolder::Export(IoProgressImplArgs) { IoProgress ErrStatus(Store3Error); if (!mimeType) { ErrStatus.errMsg = "No mimetype."; if (cb) cb(&ErrStatus, NULL); return ErrStatus; } if (!LoadThings()) { ErrStatus.errMsg = "Failed to load things."; if (cb) cb(&ErrStatus, NULL); return ErrStatus; } IoProgress Status(Store3Delayed); Status.prog = new ExportFolderTask(this, stream, mimeType, cb); return Status; } size_t ScribeFolder::Length() { if (GetItemType() == MAGIC_MAIL) { ThingList *v = View(); if (v) return v->Length(); } if (IsLoaded()) return Items.Length(); return GetItems(); } ssize_t ScribeFolder::IndexOf(Mail *m) { if (GetItemType() == MAGIC_MAIL) { ThingList *v = View(); if (v) return v->IndexOf(m); return Items.IndexOf(m); } return -1; } Mail *ScribeFolder::operator [](size_t i) { if (GetItemType() == MAGIC_MAIL) { ThingList *v = View(); if (v) return dynamic_cast(v->ItemAt(i)); Thing *t = Items[i]; if (t) return t->IsMail(); } return NULL; } bool ScribeFolder::GetVariant(const char *Name, LVariant &Value, const char *Array) { ScribeDomType Fld = StrToDom(Name); switch (Fld) { case SdType: // Type: Int32 { Value = GetObject()->Type(); break; } case SdName: // Type: String { Value = GetName(true).Get(); break; } case SdPath: // Type: String { auto p = GetPath(); if (p) Value = p; else return false; break; } case SdUnread: // Type: Int32 { Value = GetUnRead(); break; } case SdLength: // Type: Int32 { Value = (int32)Items.Length(); break; } case SdItem: // Type: Thing[] { Value.Empty(); LoadThings(); // Use in sync mode, no callback // This call back HAS to set value one way or another... if (Array) { bool IsNumeric = true; for (auto *v = Array; *v; v++) { if (!IsDigit(*v)) { IsNumeric = false; break; } } if (IsNumeric) { int Idx = atoi(Array); if (Idx >= 0 && Idx < (ssize_t)Items.Length()) { Value = (LDom*) Items[Idx]; return true; } } else // Is message ID? { for (auto t : Items) { Mail *m = t->IsMail(); if (!m) break; auto Id = m->GetMessageId(); if (Id && !strcmp(Id, Array)) { Value = (LDom*)t; return true; } } } } else if (Value.SetList()) { for (auto t : Items) Value.Value.Lst->Insert(new LVariant((LDom*)t)); return true; } break; } case SdItemType: // Type: Int32 { Value = GetItemType(); break; } case SdScribe: // Type: ScribeWnd { Value = (LDom*)App; break; } case SdChild: // Type: ScribeFolder { Value = (LDom*)GetChildFolder(); break; } case SdNext: // Type: ScribeFolder { Value = (LDom*)GetNextFolder(); break; } case SdSelected: // Type: Thing[] { if (!Select() || !Value.SetList()) return false; List a; if (!App->GetMailList()->GetSelection(a)) return false; for (auto t: a) { Value.Value.Lst->Insert(new LVariant(t)); } break; } case SdExpanded: // Type: Boolean { Value = Expanded(); break; } default: { return false; } } return true; } bool ScribeFolder::SetVariant(const char *Name, LVariant &Value, const char *Array) { ScribeDomType Fld = StrToDom(Name); switch (Fld) { case SdName: // Type: String { char *s = Value.CastString(); if (ValidStr(s)) OnRename(s); else return false; break; } case SdExpanded: // Type: Boolean { Expanded(Value.CastInt32() != 0); break; } default: return false; } return true; } bool ScribeFolder::CallMethod(const char *MethodName, LVariant *ReturnValue, LArray &Args) { ScribeDomType m = StrToDom(MethodName); switch (m) { case SdLoad: // Type: () { LoadThings(App, [&](auto Status) { *ReturnValue = Status == Store3Success; }); // Convert the async LoadFolders call back to sync. WaitForVariant(*ReturnValue); return true; } case SdSelect: // Type: () { Select(true); return true; } case SdImport: // Type: (String FileName, String MimeType) { *ReturnValue = false; if (Args.Length() != 2) LgiTrace("%s:%i - Error: expecting 2 arguments to 'Import'.\n", _FL); else { auto FileName = Args[0]->Str(); LAutoPtr f(new LFile); if (f->Open(FileName, O_READ)) { auto p = Import(AutoCast(f), Args[1]->Str()); *ReturnValue = p.status; } else LgiTrace("%s:%i - Error: Can't open '%s' for reading.\n", _FL, FileName); } break; } case SdExport: // Type: (String FileName, String MimeType) { *ReturnValue = false; if (Args.Length() != 2) LgiTrace("%s:%i - Error: expecting 2 arguments to 'Export'.\n", _FL); else { auto FileName = Args[0]->Str(); LAutoPtr f(new LFile); if (f->Open(FileName, O_WRITE)) { auto p = Export(AutoCast(f), Args[1]->Str()); *ReturnValue = p.status; } else LgiTrace("%s:%i - Error: Can't open '%s' for writing.\n", _FL, FileName); } break; } default: break; } return false; } void ScribeFolder::OnRethread() { if (GetThreaded()) { Thread(); } } diff --git a/Code/ScribeFolderProp.cpp b/Code/ScribeFolderProp.cpp --- a/Code/ScribeFolderProp.cpp +++ b/Code/ScribeFolderProp.cpp @@ -1,372 +1,372 @@ /* ** FILE: ScribeFolderProp.cpp ** AUTHOR: Matthew Allen ** DATE: 17/5/1999 ** DESCRIPTION: Scribe folder properties dialog ** ** Copyright (C) 1999, Matthew Allen ** fret@memecode.com */ #include #include #include #include #include "Scribe.h" #include "lgi/common/TextLabel.h" #include "lgi/common/ProgressDlg.h" #include "resdefs.h" #include "lgi/common/TabView.h" #include "lgi/common/LgiRes.h" ////////////////////////////////////////////////////////////////////////////// class LFolderInfo : public LListItem { public: ScribeFolder *Folder; uint64 Size; }; int FolderInfo_Compare(LListItem *a, LListItem *b, NativeInt Data) { LFolderInfo *A = dynamic_cast(a); LFolderInfo *B = dynamic_cast(b); if (A && B) { return (int) ((int64)B->Size - (int64)A->Size); } return 0; } ////////////////////////////////////////////////////////////////////////////// #define M_INIT_DONE (M_USER + 1000) class FolderPropertiesDlg : public LDialog { // Data ScribeFolder *Folder; // Controls LTabView *Tab; // LTabPage *DetailTab; LList *Usage; LView *Txt; // Scanning portion.. bool Loop; public: bool RePopulate; FolderPropertiesDlg(ScribeFolder *folder, int InitialTab) { RePopulate = false; Loop = true; Txt = 0; Folder = folder; if (!Folder) { LAssert(!"No folder."); return; } SetParent(Folder->App); if (LoadFromResource(IDD_FOLDER_PROPS)) { MoveToCenter(); GetViewById(IDC_TAB, Tab); GetViewById(IDC_FOLDER_MSG, Txt); GetViewById(IDC_USAGE, Usage); } auto Path = Folder->GetPath(); SetCtrlName(IDC_PATH, Path); ScribePerm p = Folder->GetFolderPerms(ScribeReadAccess); if (p == PermRequireAdmin) { SetCtrlEnabled(IDC_FPR_NONE, false); SetCtrlEnabled(IDC_FPR_USER, false); } else { SetCtrlEnabled(IDC_FPR_ADMIN, false); } SetCtrlValue(IDC_FOLDER_READ, p); p = Folder->GetFolderPerms(ScribeWriteAccess); if (p == PermRequireAdmin) { SetCtrlEnabled(IDC_FPW_NONE, false); SetCtrlEnabled(IDC_FPW_USER, false); } else { SetCtrlEnabled(IDC_FPW_ADMIN, false); } SetCtrlValue(IDC_FOLDER_WRITE, p); } ~FolderPropertiesDlg() { LAssert(Loop == false); } void OnCreate() { PostEvent(M_INIT_DONE); } bool OnRequestClose(bool OsClose) { if (Loop) { Loop = false; return false; } return true; } int OnNotify(LViewI *Ctrl, LNotification n) { switch (Ctrl->GetId()) { case IDOK: { auto Err = [&]() { LgiMsg(this, "Failed to set permissions.", AppName); return false; }; Folder->SetFolderPerms(this, ScribeReadAccess, (ScribePerm) GetCtrlValue(IDC_FOLDER_READ), [&](auto status) { if (!status) return Err(); this->Folder->SetFolderPerms(this, ScribeWriteAccess, (ScribePerm) this->GetCtrlValue(IDC_FOLDER_WRITE), [&](auto status) { if (!status) return Err(); EndModal(1); return true; }); return true; }); break; } case IDCANCEL: { if (Loop) Loop = false; else EndModal(0); break; } } return 0; } void AddMimeSeg(LDataPropI *Ptr, Counter &c, uint64 Size) { // Add this one... LDataI *Seg = dynamic_cast(Ptr); if (Seg) { c.Inc(Seg->Type()); Size += Seg->Size(); // Add the children... - GDataIt Children = Seg->GetList(FIELD_MIME_SEG); + LDataIt Children = Seg->GetList(FIELD_MIME_SEG); if (Children) { for (LDataPropI *Child = Children->First(); Child; Child = Children->Next()) { AddMimeSeg(Child, c, Size); } } } } void Count(LDataFolderI *f, Counter &c, int depth = 0) { uint64 Size = f->Size(); c.Inc(f->Type()); LDataIterator &fc = f->Children(); for (unsigned i=0; Loop && iType()); Size += t->Size(); if (t->Type() == MAGIC_MAIL) { AddMimeSeg(t->GetObj(FIELD_MIME_SEG), c, Size); } } if (i % 30 == 0) LYield(); } c.Add(1, Size); #ifdef _DEBUG char s[256]; memset(s, '\t', depth); s[depth] = 0; LgiTrace("%sCounting %s (size=%i)\n", s, f->GetStr(FIELD_FOLDER_NAME), Size); #endif for (unsigned n=0; Loop && nSubFolders().Length(); n++) { LDataFolderI *s = f->SubFolders()[n]; Count(s, c, depth + 1); LYield(); } } void Run() { Counter c; LYield(); // Do count LDataFolderI *f = Folder->GetFldObj(); c.Inc(f->Type()); c.Add(1, f->Size()); LDataIterator &Children = f->Children(); for (unsigned i=0; Loop && iType()); c.Add(1, t->Size()); LYield(); } for (ScribeFolder *Child = Folder->GetChildFolder(); Loop && Child; Child = Child->GetNextFolder()) { uint64 Old = c.GetTypeCount(1); Count(Child->GetFldObj(), c); if (Usage) { LFolderInfo *i = new LFolderInfo; if (i) { i->Folder = Child; i->Size = c.GetTypeCount(1) - Old; char Size[32]; LFormatSize(Size, sizeof(Size), i->Size); i->SetText(Child->GetText(), 0); i->SetText(Size, 1); Usage->Insert(i); } } LYield(); } int64 Used = c.GetTypeCount(1); // 64 bytes in the header // post count tallying if (Loop && Usage) { // set the percent for (auto it = Usage->begin(); Loop && it != Usage->end(); it++) { LFolderInfo *i = dynamic_cast(*it); if (!i) continue; char Str[32]; sprintf_s(Str, sizeof(Str), "%.1f", (double)(int64)i->Size * 100 / Used ); i->SetText(Str, 2); LYield(); } // sort the items Usage->Sort(FolderInfo_Compare); Usage->ResizeColumnsToContent(); } // other props LString MsgSize = LFormatSize(Used); char Msg[512]; const char *Format = LLoadString(IDS_FOLDER_PROPERTIES_DLG); int ch = sprintf_s(Msg, sizeof(Msg), Format?Format:"", (int) c.GetTypeCount(MAGIC_MAIL), (int) c.GetTypeCount(MAGIC_CONTACT), (int) c.GetTypeCount(MAGIC_FOLDER), MsgSize.Get()); if (Loop && !Folder->GetParent()) { LMailStore *Ms = Folder->App->GetDefaultMailStore(); if (Ms) { char File[32], Unused[32]; uint64 FileSize = Ms->Store->Size(); LFormatSize(File, sizeof(File), FileSize); LFormatSize(Unused, sizeof(Unused), FileSize-Used); sprintf_s(Msg+ch, sizeof(Msg)-ch, LLoadString(IDS_FOLDER_PROPERTIES_COMPACT), File, (int) ((Used*100)/FileSize), Unused); } } if (Txt) { Txt->Name(Msg); Txt->SendNotify(LNotifyTableLayoutRefresh); } Loop = false; } LMessage::Param OnEvent(LMessage *Msg) { if (Msg->Msg() == M_INIT_DONE) Run(); return LDialog::OnEvent(Msg); } }; ////////////////////////////////////////////////////////////////////////////// void OpenFolderProperties(ScribeFolder *Parent, int Tab, std::function callback) { auto Dlg = new FolderPropertiesDlg(Parent, Tab); Dlg->DoModal([callback, Dlg](auto dlg, auto code) { if (code && callback) callback(Dlg->RePopulate); delete dlg; }); } diff --git a/Code/ScribeFolderTree.cpp b/Code/ScribeFolderTree.cpp --- a/Code/ScribeFolderTree.cpp +++ b/Code/ScribeFolderTree.cpp @@ -1,826 +1,826 @@ /* ** FILE: ScribeFolderTree.cpp ** AUTHOR: Matthew Allen ** DATE: 17/1/2000 ** DESCRIPTION: Scribe Folder Tree ** ** Copyright (C) 2000-2002, Matthew Allen ** fret@memecode.com */ // Includes #include "Scribe.h" #include "lgi/common/DropFiles.h" #include "lgi/common/TextLabel.h" #include "lgi/common/RtfHtml.h" #include "resdefs.h" #include "ScribeListAddr.h" #include "lgi/common/ProgressDlg.h" #if WINNATIVE #include "OutlookDropSupport.cpp" #endif #include "lgi/common/LgiRes.h" ////////////////////////////////////////////////////////////////////////////// int FolderSorter(ScribeFolder *a, ScribeFolder *b, int d) { auto A = a->GetName(true); auto B = b->GetName(true); return A && B ? _stricmp(A, B) : 0; } ////////////////////////////////////////////////////////////////////////////// MailTree::MailTree(ScribeWnd *app) : LTree(100, 0, 0, 100, 100, "") { App = app; LastHit = 0; LastWasRoot = -1; Sunken(false); } MailTree::~MailTree() { } void MailTree::OnCreate() { SetWindow(this); } ssize_t MailTree::Sizeof() { LAssert(0); return 0; } bool MailTree::Serialize(LFile &f, bool Write) { bool Status = false; LAssert(0); return Status; } #ifdef _DEBUG const char *GetCompareHdrs(LDataI *Mail, LDataI *a) { auto s = a->GetStr(FIELD_INTERNET_HEADER); if (!s && Mail) s = Mail->GetStr(FIELD_INTERNET_HEADER); return s; } void CompareTrimWhite(LArray &a) { while (a.Length() > 0 && strchr(WhiteSpace, a[a.Length()-1])) a.Length(a.Length()-1); a.Add(0); } char *CompareChar(char a) { static char buf[4][16]; static int next = 0; char *b = buf[next++]; if (next >= 4) next = 0; if (a < ' ' || ((uint8_t)a) >= 0x80) sprintf_s(b, 16, "0x%2.2X", a); else sprintf_s(b, 16, "'%c'", a); return b; } struct SegMap { LDataI *a, *b; }; bool CompareSegments(LDataI *MailA, LDataI *MailB, LDataI *a, LDataI *b, LStream &p) { const char *s1, *s2; // Check headers s1 = GetCompareHdrs(MailA, a); s2 = GetCompareHdrs(MailB, b); if (s1 && s2 && strcmp(s1, s2)) { p.Print("Seg.Headers(%p,%p),", a, b); return false; } else if ((s1 != 0) ^ (s2 != 0)) { p.Print("Seg.HeaderPtrs,"); return false; } // Check data LAutoStreamI da = a->GetStream(_FL); LAutoStreamI db = b->GetStream(_FL); if (da && db) { LAutoString TransferEncoding(s1?InetGetHeaderField(s1, "Content-Transfer-Encoding"):0); bool IsText = true; if (TransferEncoding && !_stricmp(TransferEncoding, "base64")) IsText = false; if (IsText) { // Text compare LArray bufa, bufb; if (bufa.Length((uint32_t) da->GetSize()) && bufb.Length((uint32_t) db->GetSize())) { da->Read(&bufa[0], bufa.Length()); db->Read(&bufb[0], bufb.Length()); CompareTrimWhite(bufa); CompareTrimWhite(bufb); char *ta = &bufa[0], *tb = &bufb[0]; do { if (*ta != *tb) { p.Print("Seg.TextBody(%s != %s @ %i, %p, %p),", CompareChar(*ta), CompareChar(*tb), ta - &bufa[0], a, b); return false; } ta++; tb++; if (*ta == '\r') ta++; if (*tb == '\r') tb++; } while (*ta && *tb); } else { p.Print("Seg.TextMemAlloc(" LPrintfInt64 "," LPrintfInt64 "),", da->GetSize(), db->GetSize()); return false; } } else { // Binary compare if (da->GetSize() != db->GetSize()) { p.Print("Seg.DataSize(" LPrintfInt64 "," LPrintfInt64 "),", da->GetSize(), db->GetSize()); return false; } else { char bufa[1024], bufb[1024]; for (int64 i=0; iGetSize(); ) { ssize_t ra = da->Read(bufa, sizeof(bufa)); ssize_t rb = db->Read(bufb, sizeof(bufb)); if (ra == rb) { if (memcmp(bufa, bufb, ra)) { p.Print("Seg.DataCmp,"); return false; } i += ra; } else { p.Print("Seg.DataRead,"); return false; } } } } } else if ((da != 0) ^ (db != 0)) { p.Print("Seg.Data(%p[%p,%I64i],%p[%p,%I64i]),", a, da.Get(), da ? da->GetSize() : 0, b, db.Get(), db ? db->GetSize() : 0); return false; } - GDataIt ac = a->GetList(FIELD_MIME_SEG); - GDataIt bc = b->GetList(FIELD_MIME_SEG); + LDataIt ac = a->GetList(FIELD_MIME_SEG); + LDataIt bc = b->GetList(FIELD_MIME_SEG); if (ac && bc) { if (ac->Length() != bc->Length()) { p.Print("Seg.ChildLength,"); return false; } else { LArray Map; LDataI *Seg; for (Seg = dynamic_cast(ac->First()); Seg; Seg = dynamic_cast(ac->Next())) { Map.New().a = Seg; } for (Seg = dynamic_cast(bc->First()); Seg; Seg = dynamic_cast(bc->Next())) { auto hdr_b = Seg->GetStr(FIELD_INTERNET_HEADER); bool Matched = false; for (unsigned i=0; iGetStream(_FL); LAutoStreamI db = Seg->GetStream(_FL); if (da && db) { int64 size_a = da->GetSize(); int64 size_b = db->GetSize(); int64 diff = size_a - size_b; if (diff < 0) diff = -diff; if (diff == 0) Exact = true; else if (diff < 8) Close = true; } // What about headers? auto hdr_a = m.a->GetStr(FIELD_INTERNET_HEADER); if (hdr_a && hdr_b) { if (_stricmp(hdr_a, hdr_b)) { Close = false; Exact = false; } } if (Close || Exact) { // Match m.b = Seg; Matched = true; break; } } } if (!Matched) { for (unsigned i=0; iGetStr(FIELD_INTERNET_HEADER); if (hdr_a && hdr_b) { if (!_stricmp(hdr_a, hdr_b)) { // Match m.b = Seg; break; } } } } } } unsigned i; for (i=0; iGetStream(_FL); p.Print("%s%p - %s (%I64i)\r\n", sp, s, ContentType.Get(), Data?Data->GetSize():-1); - GDataIt c = s->GetList(FIELD_MIME_SEG); + LDataIt c = s->GetList(FIELD_MIME_SEG); if (c) { for (LDataI *a = dynamic_cast(c->First()); a; a = dynamic_cast(c->Next())) { CompareDumpSegs(p, 0, a, depth+1); } } if (mail) p.Print("\r\n"); } #endif void MailTree::OnItemClick(LTreeItem *Item, LMouse &m) { if (m.Down() && m.IsContextMenu()) { Select(Item); ScribeFolder *t = dynamic_cast(Item); if (t) t->DoContextMenu(m); } } void MailTree::OnItemSelect(LTreeItem *Item) { if (LastWasRoot ^ (int8)Item->IsRoot()) { if (Item->IsRoot()) { if (App->GetItemList()) App->GetItemList()->RemoveAll(); App->SetListPane(new DynamicHtml(App, "title.html")); } else { App->SetListPane(new ThingList(App)); } } if (Things() && !Item->IsRoot()) { ScribeFolder *C = dynamic_cast(Item); if (C) { C->Populate(Things()); App->SetCtrlValue(IDM_THREAD, C->GetThreaded()); App->OnFolderSelect(C); } } else { App->SetCtrlValue(IDM_THREAD, false); } LastWasRoot = Item ? Item->IsRoot() : false; } void MailTree::OnCreateSubDirectory(ScribeFolder *Item) { // setup type list... Store3ItemTypes Type[] = { MAGIC_MAIL, MAGIC_CONTACT, MAGIC_FILTER, MAGIC_CALENDAR, MAGIC_GROUP }; int i=0; for (int n=0; nGetItemType()) { i = n; break; } } // do ui.. bool Enable[] = { Item->CanHaveSubFolders(MAGIC_MAIL), Item->CanHaveSubFolders(MAGIC_CONTACT), Item->CanHaveSubFolders(MAGIC_FILTER), Item->CanHaveSubFolders(MAGIC_CALENDAR), Item->CanHaveSubFolders(MAGIC_GROUP) }; auto Dlg = new CreateSubFolderDlg(this, i, Enable); Dlg->DoModal([this, Dlg, Item, Type](auto dlg, auto code) { if (code && ValidStr(Dlg->SubName) && Dlg->SubType >= 0) { // check the name doesn't conflict.. auto Path = Item->GetPath(); if (Path) { LString s; s.Printf("%s/%s", Path.Get(), Dlg->SubName); if (App->GetFolder(s)) { LgiMsg(this, LLoadString(IDS_SUBFLD_NAME_CLASH), AppName, MB_OK); Dlg->SubName.Empty(); } } if (Dlg->SubName) { // insert the folder... Item->CreateSubDirectory(Dlg->SubName, Type[Dlg->SubType]); } } delete dlg; }); } void MailTree::OnDelete(ScribeFolder *Item, bool Force) { if (Item) { int FolderType = App->GetFolderType(Item); if (!Force && FolderType >= 0) { char Msg[256]; sprintf_s(Msg, sizeof(Msg), LLoadString(IDS_DELETE_SYS_FOLDER), DefaultFolderNames[FolderType]); LgiMsg(this, Msg, AppName, MB_OK); } else { Item->OnDelete(); } } } void MailTree::OnProperties(ScribeFolder *Item) { if (Item) { Item->OnProperties(); } } LMessage::Result MailTree::OnEvent(LMessage *Msg) { return LTree::OnEvent(Msg); } extern char ScribeFolderObject[]; int MailTree::WillAccept(LDragFormats &Formats, LPoint Pt, int KeyState) { int Status = DROPEFFECT_NONE; LastHit = ItemAtPoint(Pt.x, Pt.y); if (LastHit) { List Accepted; Formats.Supports(ScribeThingList); Formats.Supports(ScribeFolderObject); Formats.SupportsFileDrops(); #if WINNATIVE Formats.Supports(CFSTR_FILEDESCRIPTOR); #endif #ifdef MAC Formats.Supports(LGI_StreamDropFormat); #endif if (Formats.GetSupported().Length()) { SelectDropTarget(LastHit); Status = KeyState & LGI_EF_CTRL ? DROPEFFECT_COPY : DROPEFFECT_MOVE; } } return Status; } static int FolderItemCmp(LTreeItem *a, LTreeItem *b, NativeInt UserData) { ScribeFolder *ta = dynamic_cast(a); ScribeFolder *tb = dynamic_cast(b); if (ta && tb) { int64 IndexA = ta->GetObject()->GetInt(FIELD_FOLDER_INDEX); int64 IndexB = tb->GetObject()->GetInt(FIELD_FOLDER_INDEX); return (int)IndexA - (int)IndexB; } return 0; } int MailTree::OnDrop(LArray &Data, LPoint Pt, int KeyState) { int Status = DROPEFFECT_NONE; ScribeFolder *Leaf = dynamic_cast(LastHit); #if WINNATIVE LString FileDescFmt = CFSTR_FILEDESCRIPTOR; #endif SelectDropTarget(0); if (App) App->SetLastDrop(); for (unsigned idx=0; idxLength(); ScribeFolder *Root = dynamic_cast(ItemAt(0)); for (ssize_t i=0; iLength(); i++) { ScribeFolder *Folder = Fmt->FolderAt(i); if (Folder) { if (Folder == Leaf || Folder == Root || Leaf->GetItemType() == MAGIC_ANY) { // they're dragging onto themselves or // dragging the whole mail tree around or // dragging a folder into the trash // just quit out now... return DROPEFFECT_NONE; } bool CopyOp = (KeyState & LGI_EF_CTRL) != 0; auto FinishFolderOp = [&](int Res) { int SystemFolder = App->GetFolderType(Folder); auto OldPath = Folder->GetPath(); Store3Status Status = Store3Error; ScribeFolder *OldParent = Folder->GetFolder(); ScribeFolder *NewParent = NULL; int Index = 0; if (Res == 1) { // Attach next NewParent = Leaf->GetFolder(); if (NewParent) { // Work out the index for (LTreeItem *Item = NewParent->GetChild(); Item && Item!=Leaf; Item=Item->GetNext()) { Index++; } Index++; } } else if (Res == 2) { // Attach child NewParent = Leaf; } else return; if (SystemFolder >= 0 && NewParent->GetObject()->GetStore() != OldParent->GetObject()->GetStore()) { LgiMsg(App, "Can't move system folders to a different mail store.", AppName, MB_OK); return; } if (CopyOp) { // Copy Status = Folder->CopyTo(NewParent, Index); } else { LDataFolderI *fo = Folder->GetFldObj(); if (NewParent == OldParent) { // Re-index only... // int64 OldIndex = fo->GetInt(FIELD_FOLDER_INDEX); if (fo->SetInt(FIELD_FOLDER_INDEX, Index)) { // Change the UI to match... NewParent->Sort(FolderItemCmp); Status = Store3Success; } } else { // Move Status = Folder->SetFolder(NewParent, Index); if (Status) { NewParent->Sort(FolderItemCmp); } } } if (Status == Store3Success && SystemFolder >= 0 && !CopyOp) { // Update the system path location if it's changed... auto NewPath = Folder->GetPath(); if (OldPath && NewPath && strcmp(OldPath, NewPath) != 0) { LVariant v; v = NewPath; LString SysFolderName; SysFolderName.Printf("Folder-%i", SystemFolder); App->GetOptions()->SetValue(SysFolderName, v); } } }; int Res = 2; if (Leaf->GetFolder() != NULL) { auto Dlg = new LAlert( this, LLoadString(CopyOp ? IDS_COPY_FOLDER : IDS_MOVE_FOLDER), LLoadString(IDS_MOVE_ATTACH), LLoadString(IDS_NEXT_FOLDER), LLoadString(IDS_SUB_FOLDER), LLoadString(IDS_CANCEL)); Dlg->DoModal([this, Dlg, FinishFolderOp](auto dlg, auto Res) { if (Res > 0) FinishFolderOp(Res); delete dlg; }); } else FinishFolderOp(Res); } } } else if (ScribeClipboardFmt::IsThing(v.Value.Binary.Data, v.Value.Binary.Length)) { ScribeClipboardFmt *Fmt = (ScribeClipboardFmt*) v.Value.Binary.Data; LDataStoreI::StoreTrans Trans = Leaf->GetObject()->GetStore()->StartTransaction(); bool CopyOnly = (KeyState & LGI_EF_CTRL) != 0; Count = Fmt->Length(); LArray Items; for (ssize_t i=0; iThingAt(i); if (Thg) Items.Add(Thg); } if (Items.Length()) { LArray ItemStatus; Leaf->MoveTo(Items, CopyOnly, &ItemStatus); for (auto s: ItemStatus) if (s == Store3Error) Errors++; } } else LAssert(!"Unknown drop format."); App->Update(); if (Errors) { LgiMsg(this, LLoadString(IDS_MOVE_ERROR), AppName, MB_OK, Errors, Count); } else { Status = DROPEFFECT_COPY; } // We don't need to process any other data types... we're done. break; } else if (dd.IsFileDrop()) { if (dd.Data.Length() > 0) { LDropFiles Files(dd.Data[0]); Leaf->OnReceiveFiles(Files); Status = DROPEFFECT_COPY; } } #if WINNATIVE else if (_stricmp(dd.Format, FileDescFmt) == 0) { if (dd.Data.Length() == 0 || dd.Data[0].Type != GV_BINARY) continue; // Get file list... LString::Array Files; FILEGROUPDESCRIPTOR *FileGroup = (FILEGROUPDESCRIPTOR*) dd.Data[0].Value.Binary.Data; if (OnDropFileGroupDescriptor(FileGroup, Files)) { // Process dropped files LArray FileLst; for (auto &f : Files) FileLst.Add(f.Get()); Leaf->OnReceiveFiles(FileLst); // Clean up for (auto f : Files) { FileDev->Delete(f, false); } } else if (DataObject) { for (int i=0; true; i++) { FORMATETC Format; Format.cfFormat = RegisterClipboardFormat(CFSTR_FILECONTENTS); Format.dwAspect = DVASPECT_CONTENT; Format.lindex = i; Format.tymed = TYMED_ISTORAGE; Format.ptd = 0; STGMEDIUM Medium; ZeroObj(Medium); HRESULT res = DataObject->GetData(&Format, &Medium); if (SUCCEEDED(res)) { if (Medium.tymed == TYMED_ISTORAGE) { OutlookIStorage Dec(App, Medium.pstg, false); int Type = Dec.GetType(); if (Type == Leaf->GetItemType()) { switch (Type) { case MAGIC_MAIL: { Mail *m = Dec.GetMail(); if (m) { m->App = App; m->Save(Leaf); Status = DROPEFFECT_COPY; } break; } case MAGIC_CONTACT: { Contact *c = Dec.GetContact(); if (c) { c->App = App; c->Save(Leaf); Status = DROPEFFECT_COPY; } break; } } } } ReleaseStgMedium(&Medium); } else break; } } } #endif } return Status; } diff --git a/Code/ScribeMail.cpp b/Code/ScribeMail.cpp --- a/Code/ScribeMail.cpp +++ b/Code/ScribeMail.cpp @@ -1,10247 +1,10252 @@ /* ** FILE: ScribeMail.cpp ** AUTHOR: Matthew Allen ** DATE: 11/11/98 ** DESCRIPTION: Scribe Mail Object and UI ** ** Copyright (C) 1998-2003, Matthew Allen ** fret@memecode.com */ #include #include #include #include #include #include #include "Scribe.h" #include "ScribePageSetup.h" #include "lgi/common/NetTools.h" #include "lgi/common/Popup.h" #include "lgi/common/ColourSelect.h" #include "lgi/common/TextView3.h" #include "lgi/common/Html.h" #include "lgi/common/Combo.h" #include "lgi/common/Edit.h" #include "lgi/common/Button.h" #include "lgi/common/TextLabel.h" #include "lgi/common/CheckBox.h" #include "lgi/common/TabView.h" #include "lgi/common/Input.h" #include "lgi/common/ClipBoard.h" #include "lgi/common/TableLayout.h" #include "lgi/common/DisplayString.h" #include "lgi/common/ThreadEvent.h" #include "lgi/common/GdcTools.h" #include "lgi/common/Charset.h" #include "../src/common/Coding/ScriptingPriv.h" #include "PrintPreview.h" #include "ScribeListAddr.h" #include "PrintContext.h" #include "lgi/common/LgiRes.h" #include "Encryption/GnuPG.h" #include "ObjectInspector.h" #include "lgi/common/EventTargetThread.h" #include "Store3Common.h" #include "Tables.h" #include "Calendar.h" #include "CalendarView.h" #include "AddressSelect.h" #include "Store3Imap/ScribeImap.h" #include "lgi/common/TextConvert.h" #include "lgi/common/FileSelect.h" #include "resdefs.h" #include "resource.h" #include "lgi/common/Printer.h" #include "lgi/common/SubProcess.h" #define SAVE_HEADERS 0 static char MsgIdEncodeChars[] = "%/"; static char ScribeReplyClass[] = "scribe_reply"; static char ScribeReplyStyles[] = "margin-left: 0.5em;\n" "padding-left: 0.5em;\n" "border-left: 1px solid #ccc;"; char DefaultTextReplyTemplate[] = { "---------- Original Message ----------\n" "To: <>\n" "From: <>\n" "Subject: \n" "Date: \n" "\n" "\n" "\n" "\n" }; char DefaultHtmlReplyTemplate[] = { "\n" "\n" "\n" "\n" "\n" "

---------- Original Message ----------
\n" " To: <?mail.tohtml?>
\n" " From: <?mail.fromhtml?>
\n" " Subject: <?mail.subject?>
\n" " Date: <?mail.datesent?>
\n" "
\n" " <?mail.bodyastext quote=scribe.quote?>
\n" " <?cursor?>
\n" " <?mail.sig[html]?>

\n" "\n" "\n" }; class DepthCheck { int &i; public: constexpr static int MaxDepth = 5; DepthCheck(int &val) : i(val) { i++; if (!*this) LAssert(!"Recursion?"); } ~DepthCheck() { i--; } operator bool() const { return i < MaxDepth; } }; void CollectAttachments(LArray *Attachments, LArray *Related, LDataI **Text, LDataI **Html, LDataPropI *d, Store3MimeType *ParentMime = NULL) { if (!d) return; Store3MimeType Mt(d->GetStr(FIELD_MIME_TYPE)); auto FileName = d->GetStr(FIELD_NAME); if (!Mt) Mt = "text/plain"; // printf("Collect %s %s\n", (char*)Mt, FileName); - LDataI *Att = dynamic_cast(d); + auto Att = dynamic_cast(d); if (ParentMime && Att && ParentMime->IsRelated() && Related) { auto Id = d->GetStr(FIELD_CONTENT_ID); if (ValidStr(Id)) { if (Related) Related->Add(Att); Att = NULL; } else if (Mt.IsHtml() && Html) { *Html = Att; Att = NULL; } } if (Att) { if (ValidStr(FileName)) { if (Attachments) Attachments->Add(Att); } else if (Mt.IsHtml()) { if (Html) *Html = Att; } else if (Mt.IsPlainText()) { if (Text) *Text = Att; } else if (!Mt.IsMultipart()) { if (Attachments) Attachments->Add(Att); } /* if (d->GetInt(FIELD_SIZE) < (512 << 10)) a.Add(dynamic_cast(d)); */ } - GDataIt It = d->GetList(FIELD_MIME_SEG); + auto It = d->GetList(FIELD_MIME_SEG); if (It) { - for (LDataPropI *i=It->First(); i; i=It->Next()) + for (auto i = It->First(); i; i = It->Next()) CollectAttachments(Attachments, Related, Text, Html, i, Mt.IsMultipart() ? &Mt : NULL); } } void RemoveReturns(char *s) { // Delete out the '\r' chars. char *In = s; char *Out = s; while (*In) { if (*In != '\r') { *Out++ = *In; } In++; } *Out++ = 0; } class XmlSaveStyles : public LXmlTree { void OnParseComment(LXmlTag *Ref, const char *Comment, ssize_t Bytes) { if (Ref && Ref->IsTag("style")) { Ref->SetContent(Comment, Bytes); } } public: XmlSaveStyles(int flags) : LXmlTree(flags) { } }; bool ExtractHtmlContent(LString &OutHtml, LString &Charset, LString &Styles, const char *InHtml) { if (!InHtml) return false; XmlSaveStyles t(GXT_NO_ENTITIES | GXT_NO_DOM | GXT_NO_HEADER); LXmlTag r; LMemStream mem(InHtml, strlen(InHtml), false); if (!t.Read(&r, &mem)) return false; bool InHead = false; LStringPipe Style; r.Children.SetFixedLength(false); for (auto It = r.Children.begin(); It != r.Children.end(); ) { LXmlTag *c = *It; if (c->IsTag("style")) { if (ValidStr(c->GetContent())) Style.Print("%s\n", c->GetContent()); c->Parent = NULL; r.Children.Delete(It); DeleteObj(c); } else if (c->IsTag("/head")) { InHead = false; c->Parent = NULL; r.Children.Delete(It); DeleteObj(c); } else if (c->IsTag("body")) { // We remove this tag, but KEEP the content... if any if (ValidStr(c->GetContent())) { c->SetTag(NULL); It++; } else { // No content, remove entirely. c->Parent = NULL; r.Children.Delete(It); DeleteObj(c); } } else if (InHead || c->IsTag("html") || c->IsTag("/html") || c->IsTag("/body") || c->IsTag("/style")) { c->Parent = NULL; r.Children.Delete(It); DeleteObj(c); } else if (c->IsTag("head")) { InHead = true; c->Parent = NULL; r.Children.Delete(It); DeleteObj(c); } else It++; } LStringPipe p; t.Write(&r, &p); OutHtml = p.NewGStr(); Styles = Style.NewGStr(); #if 0 LgiTrace("InHtml=%s\n", InHtml); LgiTrace("OutHtml=%s\n", OutHtml.Get()); LgiTrace("Styles=%s\n", Styles.Get()); #endif return true; } ////////////////////////////////////////////////////////////////////////////// char MailToStr[] = "mailto:"; char SubjectStr[] = "subject="; char ContentTypeDefault[] = "Content-type: text/plain; charset=us-ascii"; extern LString HtmlToText(const char *Html, const char *InitialCharSet); extern LString TextToHtml(const char *Txt, const char *Charset); class ImageResizeThread : public LEventTargetThread { LOptionsFile *Opts; public: class Job { #ifdef __GTK_H__ /* This object may not exist when the worker is finished. However LAppInst->PostEvent can handle that so we'll allow it, so long as it's never used in the Sink->PostEvent form. */ LViewI *Sink; #else OsView Sink; #endif public: LString FileName; LAutoStreamI Data; void SetSink(LViewI *v) { #if !LGI_VIEW_HANDLE Sink = v; #else Sink = v->Handle(); #endif } bool PostEvent(int Msg, LMessage::Param a = 0, LMessage::Param b = 0) { #ifdef __GTK_H__ return LAppInst->PostEvent(Sink, Msg, a, b); #else return LPostEvent(Sink, Msg, a, b); #endif } }; ImageResizeThread(LOptionsFile *opts) : LEventTargetThread("ImageResize") { Opts = opts; } void Resize(LAutoPtr &Job) { LVariant Qual = 80, Px = 1024, SizeLimit = 200, v; Opts->GetValue(OPT_ResizeJpegQual, Qual); Opts->GetValue(OPT_ResizeMaxPx, Px); Opts->GetValue(OPT_ResizeMaxKb, SizeLimit); LAutoStreamI Input = Job->Data; LAutoStreamI MemBuf(new LMemFile(4 << 10)); int64 FileSize = Input->GetSize(); LStream *sImg = dynamic_cast(Input.Get()); LAutoPtr Img(GdcD->Load(sImg, Job->FileName)); if (Img) { int iPx = Px.CastInt32(); int iKb = SizeLimit.CastInt32(); if (Img->X() > iPx || Img->Y() > iPx || FileSize >= iKb << 10) { // Create a JPEG filter auto Jpeg = LFilterFactory::New(".jpg", FILTER_CAP_WRITE, NULL); if (Jpeg) { // Re-sample the image... double XScale = (double) Img->X() / iPx; double YScale = (double) Img->Y() / iPx; // double Aspect = (double) Img->X() / Img->Y(); double Scale = XScale > YScale ? XScale : YScale; if (Scale > 1.0) { int Nx = (int)(Img->X() / Scale + 0.001); int Ny = (int)(Img->Y() / Scale + 0.001); LAutoPtr ResizedImg(new LMemDC(Nx, Ny, Img->GetColourSpace())); if (ResizedImg) { if (ResampleDC(ResizedImg, Img)) { Img = ResizedImg; } } } // Compress the image.. LXmlTag Props; Props.SetValue(LGI_FILTER_QUALITY, Qual); Props.SetValue(LGI_FILTER_SUBSAMPLE, v = 1); // 2x2 Jpeg->Props = &Props; if (Jpeg->WriteImage(dynamic_cast(MemBuf.Get()), Img) == LFilter::IoSuccess) { Job->Data = MemBuf; } } } } Job->PostEvent(M_RESIZE_IMAGE, (LMessage::Param)Job.Get()); Job.Release(); // Do this after the post event... so the deref doesn't crash. } LMessage::Result OnEvent(LMessage *Msg) { switch (Msg->Msg()) { case M_RESIZE_IMAGE: { LAutoPtr j((Job*)Msg->A()); if (j) Resize(j); break; } } return 0; } }; #include "lgi/common/RichTextEdit.h" #include "../src/common/Widgets/Editor/RichTextEditPriv.h" class MailRendererScript : public LThread, public LCancel, public LDom { Mail *m; LScriptCallback *cb; public: MailRendererScript(Mail *ml, LScriptCallback *script) : m(ml), cb(script), LThread("MailRendererScript") { Run(); } ~MailRendererScript() { Cancel(true); while (!IsExited()) LSleep(1); } int Main() { LVirtualMachine Vm; LScriptArguments Args(&Vm); Args.New() = new LVariant(m->App); Args.New() = new LVariant(m); Args.New() = new LVariant((LDom*)this); m->App->ExecuteScriptCallback(*cb, Args); Args.DeleteObjects(); return 0; } bool CallMethod(const char *MethodName, LVariant *Ret, LArray &Args) { ScribeDomType Method = StrToDom(MethodName); *Ret = false; switch (Method) { case SdGetTemp: { *Ret = ScribeTempPath(); break; } case SdExecute: { *Ret = false; if (Args.Length() >= 3) { auto Dir = Args[0]->Str(); auto Exe = Args[1]->Str(); auto Arg = Args[2]->Str(); if (Dir && Exe && Arg) { LSubProcess p(Exe, Arg); p.SetInitFolder(Dir); if (p.Start()) { char Buf[256]; LStringPipe Out; Out.Print("%s %s\n", Exe, Arg); ssize_t Rd; while (p.IsRunning() && !IsCancelled()) { Rd = p.Read(Buf, sizeof(Buf)); if (Rd < 0) break; if (Rd == 0) LSleep(1); else Out.Write(Buf, Rd); } while (!IsCancelled() && (Rd = p.Read(Buf, sizeof(Buf))) > 0) Out.Write(Buf, Rd); // LgiTrace("%s:%i - Process:\n%s\n", _FL, Out.NewGStr().Get()); if (p.IsRunning()) p.Kill(); else *Ret = true; } } } break; } case SdSetHtml: { if (!IsCancelled() && Args.Length() > 0) { LView *Ui = m->GetUI(); if (!Ui) { // Maybe hasn't finished opening the UI? LSleep(100); Ui = m->GetUI(); } if (!Ui) Ui = m->App; if (Ui) Ui->PostEvent(M_SET_HTML, (LMessage::Param)new LString(Args[0]->Str())); } break; } default: LAssert(!"Unsupported call."); return false; } return true; } }; class MailPrivate { LAutoPtr Resizer; Mail *m; public: struct HtmlBody { LString Html, Charset, Styles; }; LAutoPtr Body; LAutoPtr Renderer; LString MsgIdCache; LString DomainCache; int InSetFlags = 0; int InSetFlagsCache = 0; MailPrivate(Mail *mail) { m = mail; } void OnSave() { Resizer.Reset(); } HtmlBody *GetBody() { if (!Body && !Body.Reset(new HtmlBody)) return NULL; if (ValidStr(m->GetHtml())) { ExtractHtmlContent( Body->Html, Body->Charset, Body->Styles, m->GetHtml()); } else if (ValidStr(m->GetBody())) { Body->Charset = m->GetCharSet(); Body->Html = TextToHtml(m->GetBody(), Body->Charset); } return Body; } bool AddResizeImage(Attachment *File) { if (!File || !m->GetUI()) return false; if (!Resizer) Resizer.Reset(new ImageResizeThread(m->App->GetOptions())); if (!Resizer) return false; LAutoStreamI Obj = File->GetObject()->GetStream(_FL); if (!Obj) return false; ImageResizeThread::Job *j = new ImageResizeThread::Job; if (!j) return false; // The user interface will handle the response... j->SetSink(m->GetUI()); j->FileName = File->GetName(); // Make a complete copy of the stream... j->Data.Reset(new LMemStream(Obj, 0, -1)); // Post the work over to the thread... Resizer->PostEvent(M_RESIZE_IMAGE, (LMessage::Param)j); // Mark the image resizing File->SetIsResizing(true); return true; } }; bool Mail::ResizeImage(Attachment *a) { return d->AddResizeImage(a); } AttachmentList::AttachmentList(int id, int x, int y, int cx, int cy, MailUi *ui) : LList(id, x, y, cx, cy, 0) { Ui = ui; } AttachmentList::~AttachmentList() { RemoveAll(); } void AttachmentList::OnItemClick(LListItem *Item, LMouse &m) { LList::OnItemClick(Item, m); if (!Item && m.IsContextMenu() && Ui) { LSubMenu RClick; RClick.AppendItem(LLoadString(IDS_ATTACH_FILE), IDM_OPEN, true); switch (RClick.Float(this, m)) { case IDM_OPEN: { Ui->PostEvent(M_COMMAND, IDM_ATTACH_FILE, 0); break; } } } } bool AttachmentList::OnKey(LKey &k) { if (k.vkey == LK_DELETE) { if (k.Down()) { List s; if (GetSelection(s)) { Attachment *a = dynamic_cast(s[0]); if (a) { a->OnDeleteAttachment(this, true); } } } return true; } return LList::OnKey(k); } ////////////////////////////////////////////////////////////////////////////// int Strnlen(const char *s, int n) { int i = 0; if (s) { if (n < 0) { while (*s++) { i++; } } else { while (*s++ && n-- > 0) { i++; } } } return i; } ////////////////////////////////////////////////////////////////////////////// MailContainer::~MailContainer() { MailContainerIter *i; while ((i = Iters[0])) { Iters.Delete(i); if (i->Container) { i->Container = 0; } } } MailContainerIter::MailContainerIter() { Container = 0; } MailContainerIter::~MailContainerIter() { if (Container) { Container->Iters.Delete(this); } } void MailContainerIter::SetContainer(MailContainer *c) { Container = c; if (Container) { Container->Iters.Insert(this); } } ////////////////////////////////////////////////////////////////////////////// uint32_t MarkColours32[IDM_MARK_MAX] = { Rgb32(255, 0, 0), // red Rgb32(255, 166, 0), // orange Rgb32(255, 222, 0), // yellow Rgb32(0, 0xa0, 0), // green Rgb32(0, 0xc0, 255),// cyan Rgb32(0, 0, 255), // blue Rgb32(192, 0, 255), // purple Rgb32(0, 0, 0) // black }; ItemFieldDef MailFieldDefs[] = { {"To", SdTo, GV_STRING, FIELD_TO}, {"From", SdFrom, GV_STRING, FIELD_FROM}, {"Subject", SdSubject, GV_STRING, FIELD_SUBJECT}, {"Size", SdSize, GV_INT64, FIELD_SIZE}, {"Received Date", SdReceivedDate, GV_DATETIME, FIELD_DATE_RECEIVED}, {"Send Date", SdSendDate, GV_DATETIME, FIELD_DATE_SENT}, {"Body", SdBody, GV_STRING, FIELD_TEXT}, {"Internet Header", SdInternetHeader, GV_STRING, FIELD_INTERNET_HEADER, IDC_INTERNET_HEADER}, {"Message ID", SdMessageID, GV_STRING, FIELD_MESSAGE_ID}, {"Priority", SdPriority, GV_INT32, FIELD_PRIORITY}, {"Flags", SdFlags, GV_INT32, FIELD_FLAGS}, {"Html", SdHtml, GV_STRING, FIELD_ALTERNATE_HTML}, {"Label", SdLabel, GV_STRING, FIELD_LABEL}, {"From Contact", SdContact, GV_STRING, FIELD_FROM_CONTACT_NAME}, {"File", SdFile, GV_STRING, FIELD_CACHE_FILENAME}, {"ImapFlags", SdImapFlags, GV_STRING, FIELD_CACHE_FLAGS}, {"ImapSeq", SdFile, GV_INT32, FIELD_IMAP_SEQ}, {"ImapUid", SdImapFlags, GV_INT32, FIELD_SERVER_UID}, {"ReceivedDomain", SdReceivedDomain, GV_STRING, FIELD_RECEIVED_DOMAIN}, {"MessageId", SdMessageId, GV_STRING, FIELD_MESSAGE_ID}, {0} }; ////////////////////////////////////////////////////////////////////////////// class LIdentityItem : public LListItem { ScribeWnd *App; ScribeAccount *Acc; char *Txt; public: LIdentityItem(ScribeWnd *app, ScribeAccount *acc) { App = app; Acc = acc; Txt = 0; LVariant e, n; if (Acc) { n = Acc->Identity.Name(); e = Acc->Identity.Email(); } else { LAssert(!"No account specified"); } if (e.Str() && n.Str()) { char t[256]; sprintf_s(t, sizeof(t), "%s <%s>", n.Str(), e.Str()); Txt = NewStr(t); } else if (e.Str()) { Txt = NewStr(e.Str()); } else if (n.Str()) { Txt = NewStr(n.Str()); } else { Txt = NewStr("(error)"); } } ~LIdentityItem() { DeleteArray(Txt); } ScribeAccount *GetAccount() { return Acc; } const char *GetText(int i) { switch (i) { case 0: { return Txt; break; } } return 0; } }; class LIdentityDropDrop : public LPopup { ScribeWnd *App; Mail *Email; LList *Lst; public: LIdentityDropDrop(ScribeWnd *app, Mail *mail, LView *owner) : LPopup(owner) { App = app; Email = mail; LRect r(0, 0, 300, 100); SetPos(r); Children.Insert(Lst = new LList(IDC_LIST, 2, 2, X()-4, Y()-4)); if (Lst) { Lst->SetParent(this); Lst->AddColumn("Identity", Lst->GetClient().X()); Lst->MultiSelect(false); if (App) { Lst->Insert(new LIdentityItem(App, 0)); for (auto a : *App->GetAccounts()) { if (a->Identity.Name().Str()) { Lst->Insert(new LIdentityItem(App, a)); } } /* for (LListItem *i = List->First(); i; i = List->Next()) { LIdentityItem *Item = dynamic_cast(i); if (Item) { char *IdEmail = a->Send.IdentityEmail(); if (Email && IdEmail && Email->From->Addr && _stricmp(Email->From->Addr, IdEmail) == 0) { Item->Select(true); } } } */ } } } void OnPaint(LSurface *pDC) { LRect r(GetClient()); LWideBorder(pDC, r, DefaultRaisedEdge); } int OnNotify(LViewI *c, LNotification n) { switch (c->GetId()) { case IDC_LIST: { if (n.Type == LNotifyItemClick) { Visible(false); LIdentityItem *NewFrom = dynamic_cast(Lst->GetSelected()); if (Email && NewFrom) { // ScribeAccount *a = NewFrom->GetAccount(); if (Email->GetUI()) { LList *FromList; if (GetViewById(IDC_FROM, FromList)) { // Change item data /* FIXME DeleteArray(Email->From->Name); DeleteArray(Email->From->Addr); DeleteArray(Email->Reply->Name); DeleteArray(Email->Reply->Addr); if (a) { Email->From->Addr = NewStr(a->Send.IdentityEmail().Str()); Email->From->Name = NewStr(a->Send.IdentityName().Str()); Email->Reply->Addr = NewStr(a->Send.IdentityReplyTo().Str()); if (Email->Reply->Addr) { Email->Reply->Name = NewStr(Email->From->Name); } } else { LVariant e, n, r; App->GetOptions()->GetValue(OPT_EmailAddr, e); App->GetOptions()->GetValue(OPT_UserName, n); App->GetOptions()->GetValue(OPT_ReplyToEmail, n); Email->From->Addr = NewStr(e.Str()); Email->From->Name = NewStr(n.Str()); Email->Reply->Addr = NewStr(r.Str()); if (Email->Reply->Addr) { Email->Reply->Name = NewStr(Email->From->Name); } } // Change UI FromList->Empty(); LDataPropI *na = new LDataPropI(Email->From); if (na) { na->CC = MAIL_ADDR_FROM; FromList->Insert(na); } */ } } } } break; } } return 0; } }; ////////////////////////////////////////////////////////////////////////////// LSubMenu* BuildMarkMenu( LSubMenu *MarkMenu, MarkedState MarkState, uint32_t SelectedMark, bool None, bool All, bool Select) { int SelectedIndex = -1; // Build image list LImageList *ImgLst = new LImageList(16, 16); if (ImgLst && ImgLst->Create(16 * CountOf(MarkColours32), 16, System32BitColourSpace)) { // ImgLst->Colour(1); ImgLst->Colour(L_MED); ImgLst->Rectangle(); for (int i=0; iColour(L_LOW); ImgLst->Box(i*16+1, 0, i*16+15, 14); SelectedIndex = i; } ImgLst->Colour(MarkColours32[i], 32); ImgLst->Rectangle(i*16+3, 2, i*16+13, 12); } } // Build Submenu if (MarkMenu && ImgLst) { ImgLst->Update(-1); MarkMenu->SetImageList(ImgLst); LMenuItem *Item = NULL; if (None) { Item = MarkMenu->AppendItem(LLoadString(IDS_NONE), Select ? IDM_SELECT_NONE : IDM_UNMARK, MarkState != MS_None); } if (All) { Item = MarkMenu->AppendItem(LLoadString(IDS_ALL), Select ? IDM_SELECT_ALL : IDM_MARK_ALL, true); } if (Item) { MarkMenu->AppendSeparator(); } for (int i=0; iAppendItem(s, ((Select) ? IDM_MARK_SELECT_BASE : IDM_MARK_BASE) + i, (MarkState != 1) || (i != SelectedIndex)); if (Item) { Item->Icon(i); } } } return MarkMenu; } ////////////////////////////////////////////////////////////////////////////// char *WrapLines(char *Str, int Len, int WrapColumn) { if (Str && Len > 0 && WrapColumn > 0) { LMemQueue Temp; int LastWhite = -1; int StartLine = 0; int XPos = 0; int i; for (i=0; Str[i] && i= WrapColumn && Len > 0) { Temp.Write((uchar*) Str+StartLine, Len); Temp.Write((uchar*) "\n", 1); XPos = 0; StartLine = StartLine + Len + 1; LastWhite = -1; } else { LastWhite = i; XPos++; } } else if (Str[i] == '\t') { XPos = ((XPos + 7) / 8) * 8; } else if (Str[i] == '\n') { Temp.Write((uchar*) Str+StartLine, i - StartLine + 1); XPos = 0; StartLine = i+1; } else { XPos++; } } Temp.Write((uchar*) Str+StartLine, i - StartLine + 1); int WrapLen = (int)Temp.GetSize(); char *Wrapped = new char[WrapLen+1]; if (Wrapped) { Temp.Read((uchar*) Wrapped, WrapLen); Wrapped[WrapLen] = 0; return Wrapped; } } return Str; } char *DeHtml(const char *Str) { char *r = 0; if (Str) { LMemQueue Buf; char Buffer[256]; auto s = Str; while (s && *s) { // Search for start of next tag const char *Start = s; const char *End = Start; for (; *End && *End != '<'; End++); // Push pre-tag data onto pipe size_t Len = End-Start; for (size_t i=0; i, ItemFieldDef*> Lut(256); if (Lut.Length() == 0) { for (ItemFieldDef **l=FieldLists; *l; l++) { for (ItemFieldDef *i = *l; i->FieldId; i++) { if (i->Option) Lut.Add(i->Option, i); } } } return (ItemFieldDef*) Lut.Find(Name); } return 0; } static bool FieldLutInit = false; static LArray IdToFieldLut; ItemFieldDef *GetFieldDefById(int Id) { if (!FieldLutInit) { FieldLutInit = true; for (ItemFieldDef **l=FieldLists; *l; l++) { for (ItemFieldDef *i = *l; i->FieldId; i++) { IdToFieldLut[i->FieldId] = i; } } } if (Id >= 0 && Id < (int)IdToFieldLut.Length()) return IdToFieldLut[Id]; return 0; } ////////////////////////////////////////////////////////////////////////////// void Log(char *File, char *Str, ...) { #if defined WIN32 const char *DefFile = "c:\\temp\\list.txt"; #else const char *DefFile = "/home/list.txt"; #endif if (Str) { LFile f; if (f.Open((File) ? File : DefFile, O_WRITE)) { char Buf[1024]; va_list Arg; va_start(Arg, Str); vsprintf_s(Buf, sizeof(Buf), Str, Arg); va_end(Arg); f.Seek(0, SEEK_END); f.Write(Buf, (int)strlen(Buf)); } } else { LFile f; if (f.Open((File) ? File : DefFile, O_WRITE)) { f.SetSize(0); } } } char *NewPropStr(LOptionsFile *Options, char *Name) { LVariant n; if (Options->GetValue(Name, n) && n.Str() && strlen(n.Str()) > 0) { return NewStr(n.Str()); } return 0; } ////////////////////////////////////////////////////////////////////////////// // Columns of controls #define MAILUI_Y 0 #define IDM_REMOVE_GRTH 1000 #define IDM_REMOVE_GRTH_SP 1001 #define IDM_REMOVE_HTML 1002 #define IDM_CONVERT_B64_TO_BIN 1003 #define IDM_CONVERT_BIN_TO_B64 1004 #define RECIP_SX 500 #define ADD_X (RECIP_X + RECIP_SX + 10) #ifdef MAC #define ADD_RECIP_BTN_X 36 #else #define ADD_RECIP_BTN_X 20 #endif #define CONTENT_BORDER 3 #if defined WIN32 #define DLG_X 15 #define DLG_Y 30 #else #define DLG_X 6 #define DLG_Y 6 #endif MailUi::MailUi(Mail *item, MailContainer *container) : ThingUi(item, LLoadString(IDS_MAIL_MESSAGE)), WorkingDlg(NULL), Sx(0), Sy(0), CmdAfterResize(NULL), MissingCaps(NULL), BtnPrev(NULL), BtnNext(NULL), BtnSend(NULL), BtnSave(NULL), BtnSaveClose(NULL), BtnAttach(NULL), BtnReply(NULL), BtnReplyAll(NULL), BtnForward(NULL), BtnBounce(NULL), GpgUi(NULL), ToPanel(NULL), Entry(NULL), Browse(NULL), SetTo(NULL), To(NULL), Remove(NULL), FromPanel(NULL), FromList(NULL), FromCbo(NULL), ReplyToPanel(NULL), ReplyToChk(NULL), ReplyToCbo(NULL), SubjectPanel(NULL), Subject(NULL), CalendarPanel(NULL), CalPanelStatus(NULL), Tab(NULL), TabText(NULL), TextView(NULL), TabHtml(NULL), HtmlView(NULL), TabAttachments(NULL), Attachments(NULL), TabHeader(NULL), Header(NULL) { // Init everything to 0 Container = container; if (!item || !item->App) { LAssert(!"Invalid ptrs"); return; } AddMode = MAIL_ADDR_TO; CurrentEditCtrl = -1; MetaFieldsDirty = IgnoreShowImgNotify = HtmlCtrlDirty = TextCtrlDirty = TextLoaded = HtmlLoaded = false; // This allows us to hook iconv conversion events LFontSystem::Inst()->Register(this); // This allows us to hook missing image library events GdcD->Register(this); // Read/Write access bool ReadOnly = !TestFlag(GetItem()->GetFlags(), MAIL_CREATED | MAIL_BOUNCE); int MinButY = 0; // Get position LRect r(150, 150, 800, 750); SetPos(r); int FontHeight = GetFont()->GetHeight(); LVariant v; LOptionsFile *Options = App ? App->GetOptions() : 0; if (Options) { if (Options->GetValue("MailUI.Pos", v)) { r.SetStr(v.Str()); } } SetPos(r); MoveSameScreen(App); Name(LLoadString(IDS_MAIL_MESSAGE)); #if WINNATIVE CreateClassW32("Scribe::MailUi", LoadIcon(LProcessInst(), MAKEINTRESOURCE(IDI_MAIL))); #endif bool IsCreated = TestFlag(GetItem()->GetFlags(), MAIL_CREATED); if (Attach(0)) { DropTarget(true); // Setup main toolbar Commands.Toolbar = App->LoadToolbar(this, App->GetResourceFile(ResToolbarFile), App->GetToolbarImgList()); if (Commands.Toolbar) { Commands.Toolbar->Raised(false); Commands.Toolbar->Attach(this); BtnSend = Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_SEND)), IDM_SEND_MSG, TBT_PUSH, !ReadOnly, IMG_SEND); Commands.Toolbar->AppendSeparator(); BtnSave = Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_SAVE)), IDM_SAVE, TBT_PUSH, !ReadOnly, IMG_SAVE); BtnSaveClose = Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_SAVE_CLOSE)), IDM_SAVE_CLOSE, TBT_PUSH, !ReadOnly, IMG_SAVE_AND_CLOSE); Commands.Toolbar->AppendSeparator(); Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_DELETE)), IDM_DELETE_MSG, TBT_PUSH, true, IMG_TRASH); Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_SPAM)), IDM_DELETE_AS_SPAM, TBT_PUSH, true, IMG_DELETE_SPAM); BtnAttach = Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_ATTACH_FILE)), IDM_ATTACH_FILE, TBT_PUSH, !ReadOnly, IMG_ATTACH_FILE); Commands.Toolbar->AppendSeparator(); BtnReply = Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_REPLY)), IDM_REPLY, TBT_PUSH, ReadOnly, IMG_REPLY); BtnReplyAll = Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_REPLYALL)), IDM_REPLY_ALL, TBT_PUSH, ReadOnly, IMG_REPLY_ALL); BtnForward = Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_FORWARD)), IDM_FORWARD, TBT_PUSH, ReadOnly, IMG_FORWARD); BtnBounce = Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_BOUNCE)), IDM_BOUNCE, TBT_PUSH, ReadOnly, IMG_BOUNCE); Commands.Toolbar->AppendSeparator(); BtnPrev = Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_PREV_MSG)), IDM_PREV_MSG, TBT_PUSH, true, IMG_PREV_ITEM); BtnNext = Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_NEXT_MSG)), IDM_NEXT_MSG, TBT_PUSH, true, IMG_NEXT_ITEM); Commands.Toolbar->AppendSeparator(); Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_HIGH_PRIORITY)), IDM_HIGH_PRIORITY, TBT_TOGGLE, true, IMG_HIGH_PRIORITY); Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_LOW_PRIORITY)), IDM_LOW_PRIORITY, TBT_TOGGLE, true, IMG_LOW_PRIORITY); Commands.Toolbar->AppendSeparator(); Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_READ_RECEIPT)), IDM_READ_RECEIPT, TBT_TOGGLE, true, IMG_READ_RECEIPT); Commands.Toolbar->AppendSeparator(); Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_PRINT)), IDM_PRINT, TBT_PUSH, true, IMG_PRINT); for (LViewI *w: Commands.Toolbar->IterateViews()) MinButY = MAX(MinButY, w->Y()); Commands.Toolbar->Customizable(App->GetOptions(), "MailWindowToolbar"); Commands.SetupCallbacks(App, this, GetItem(), LThingUiToolbar); } // Setup to/from panels LVariant AlwaysShowFrom; if (IsCreated) App->GetOptions()->GetValue(OPT_MailShowFrom, AlwaysShowFrom); bool ShowFrom = !IsCreated && !TestFlag(GetItem()->GetFlags(), MAIL_BOUNCE); LDisplayString Recip(LSysFont, LLoadString(IDS_RECIPIENTS)); LAutoPtr ReplyChk(new LCheckBox(IDC_USE_REPLY_TO, 21, #ifdef MAC 1, #else 6, #endif -1, -1, "Reply To")); int ReplyChkPx = ReplyChk ? ReplyChk->X() : 0; int RECIP_X = 20 + MAX(ReplyChkPx, Recip.X()) + 10; int EditHeight = FontHeight + 6; LVariant HideGpg; if (!item->App->GetOptions()->GetValue(OPT_HideGnuPG, HideGpg) || HideGpg.CastInt32() == 0) { GpgUi = new MailUiGpg( App, this, 21, RECIP_X, !ReadOnly && !TestFlag(GetItem()->GetFlags(), MAIL_SENT)); if (GpgUi) GpgUi->Attach(this); } ToPanel = new LPanel(LLoadString(IDS_TO), FontHeight * 8, !ShowFrom); if (ToPanel) { int Cy = 4; ToPanel->AddView(SetTo = new LCombo(IDC_SET_TO, 20, Cy, 60, EditHeight, 0)); if (SetTo) { SetTo->Insert(LLoadString(IDS_TO)); SetTo->Insert(LLoadString(IDS_CC)); SetTo->Insert(LLoadString(IDS_BCC)); if (SetTo->GetPos().x2 + 10 > RECIP_X) RECIP_X = SetTo->GetPos().x2 + 10; } ToPanel->AddView(Entry = new LEdit(IDC_ENTRY, RECIP_X, Cy, RECIP_SX, EditHeight, "")); Cy = SetTo->GetPos().y2 + 5; ToPanel->AddView(To = new AddressList(App, IDC_TO, RECIP_X, Cy, RECIP_SX, ToPanel->GetOpenSize() - Cy - 6)); if (To) To->SetImageList(App->GetIconImgList(), false); ToPanel->AddView(new LTextLabel(IDC_STATIC, 20, Cy, -1, -1, LLoadString(IDS_RECIPIENTS))); ToPanel->Raised(false); ToPanel->Attach(this); } FromPanel = new LPanel(LLoadString(IDS_FROM), FontHeight + 13, AlwaysShowFrom.CastInt32() || ShowFrom); if (FromPanel) { FromPanel->AddView(new LTextLabel(IDC_STATIC, 21, #ifdef MAC 8, #else 4, #endif -1, -1, LLoadString(IDS_FROM))); if (ShowFrom) { FromPanel->AddView(FromList = new AddressList(App, IDC_FROM, RECIP_X, 2, RECIP_SX, EditHeight)); } else { FromPanel->AddView(FromCbo = new LCombo(IDC_FROM, RECIP_X, 2, RECIP_SX, EditHeight, 0)); } if (IsCreated) { LCheckBox *Chk; auto Label = LLoadString(IDS_ALWAYS_SHOW); FromPanel->AddView(Chk = new LCheckBox(IDC_SHOW_FROM, ADD_X, #ifdef MAC 1, #else 6, #endif -1, -1, Label)); if (Chk) Chk->Value(AlwaysShowFrom.CastInt32()); } FromPanel->Raised(false); FromPanel->Attach(this); } ReplyToPanel = new LPanel("Reply To:", FontHeight + 13, false); if (ReplyToPanel) { ReplyToPanel->Raised(false); ReplyToPanel->AddView(ReplyToChk = ReplyChk.Release()); ReplyToPanel->AddView(ReplyToCbo = new LCombo(IDC_REPLY_TO_ADDR, RECIP_X, 2, RECIP_SX, EditHeight, 0)); ReplyToPanel->Attach(this); } SubjectPanel = new LPanel(LLoadString(IDS_SUBJECT), -(LSysFont->GetHeight() + 16)); if (SubjectPanel) { SubjectPanel->Raised(false); SubjectPanel->AddView( new LTextLabel(IDC_STATIC, 21, 8, -1, -1, LLoadString(IDS_SUBJECT))); SubjectPanel->AddView(Subject = new LEdit(IDC_SUBJECT, RECIP_X, 4, RECIP_SX, EditHeight, "")); SubjectPanel->Attach(this); } CalendarPanel = new LPanel(LLoadString(IDC_CALENDAR), -(LSysFont->GetHeight() + 16), false); if (CalendarPanel) { CalendarPanel->Raised(false); LTableLayout *t = new LTableLayout(IDC_TABLE); if (t) { t->GetCss(true)->Margin(LCss::Len(LCss::LenPx, LTableLayout::CellSpacing)); t->GetCss()->Width(LCss::LenAuto); t->GetCss()->Height(LCss::LenAuto); CalendarPanel->AddView(t); auto c = t->GetCell(0, 0); c->Width(LCss::Len(LCss::LenPx, RECIP_X - (LTableLayout::CellSpacing * 2))); c->PaddingLeft(LCss::Len(LCss::LenPx, 21 - LTableLayout::CellSpacing)); c->Debug = true; c->Add(new LTextLabel(IDC_STATIC, 0, 0, -1, -1, LLoadString(IDS_CALENDAR))); c = t->GetCell(1, 0); c->Add(new LButton(IDC_ADD_CAL_EVENT, 0, 0, -1, -1, LLoadString(IDS_ADD_CAL))); c = t->GetCell(2, 0); c->Add(new LButton(IDC_ADD_CAL_EVENT_POPUP, 0, 0, -1, -1, LLoadString(IDS_ADD_CAL_POPUP))); c = t->GetCell(3, 0); c->Add(CalPanelStatus = new LTextLabel(IDC_STATIC, 0, 0, -1, -1, NULL)); } CalendarPanel->Attach(this); } Tab = new LTabView(IDC_MAIL_UI_TABS, 0, 0, 1000, 1000, 0); if (Tab) { Tab->GetCss(true)->PaddingTop("3px"); // Don't attach text and html controls here, because OnLoad will do it later... TabText = Tab->Append(LLoadString(IDS_TEXT)); TabHtml = Tab->Append("HTML"); TabAttachments = Tab->Append(LLoadString(IDS_ATTACHMENTS)); TabHeader = Tab->Append(LLoadString(IDS_INTERNETHEADER)); if (TabAttachments) { TabAttachments->Append(Attachments = new AttachmentList(IDC_ATTACHMENTS, CONTENT_BORDER, CONTENT_BORDER, 200, 200, this)); if (Attachments) { Attachments->AddColumn(LLoadString(IDS_FILE_NAME), 160); Attachments->AddColumn(LLoadString(IDS_SIZE), 80); Attachments->AddColumn(LLoadString(IDS_MIME_TYPE), 100); Attachments->AddColumn(LLoadString(IDS_CONTENT_ID), 250); } } if (TabHeader) { TabHeader->Append(Header = new LTextView3( IDC_INTERNET_HEADER, CONTENT_BORDER, CONTENT_BORDER, 100, 20)); if (Header) { Header->Sunken(true); } } LTabPage *Fields = Tab->Append(LLoadString(IDS_FIELDS)); if (Fields) { Fields->LoadFromResource(IDD_MAIL_FIELDS); LTableLayout *l; if (GetViewById(IDC_TABLE, l)) { l->SetPourLargest(false); } } Tab->Attach(this); // Colours LColourSelect *Colour; if (GetViewById(IDC_COLOUR, Colour)) { LArray c32; for (int i=0; iSetColourList(&c32); } else LAssert(!"No colour control?"); } PourAll(); if (Commands.Toolbar) { if (ToPanel) ToPanel->SetClosedSize(Commands.Toolbar->Y()-1); if (FromPanel) FromPanel->SetClosedSize(Commands.Toolbar->Y()-1); if (ReplyToPanel) ReplyToPanel->SetClosedSize(Commands.Toolbar->Y()-1); } SetIcon("About64px.png"); OnLoad(); _Running = true; Visible(true); RegisterHook(this, LKeyEvents); LResources::StyleElement(this); } } MailUi::~MailUi() { DeleteObj(TextView); if (Attachments) { Attachments->RemoveAll(); } LOptionsFile *Options = (GetItem() && App) ? App->GetOptions() : 0; if (Options) { LRect p = GetPos(); if (p.x1 >= 0 && p.y1 >= 0) { LVariant v = p.GetStr(); Options->SetValue("MailUI.Pos", v); } } Tab = 0; if (GetItem()) { GetItem()->Ui = NULL; } else LgiTrace("%s:%i - Error: no item to clear UI ptr?\n", _FL); // We delete the LView here because objects // need to have their virtual tables intact _Delete(); } Mail *MailUi::GetItem() { return _Item ? _Item->IsMail() : 0; } void MailUi::SetItem(Mail *m) { if (_Item) { Mail *old = _Item->IsMail(); if (old) old->Ui = NULL; else LAssert(0); _Item = NULL; } if (m) { _Item = m; m->Ui = this; } } bool MailUi::SetDirty(bool Dirty, bool Ui) { bool b = ThingUi::SetDirty(Dirty, Ui); if (MetaFieldsDirty && !Dirty) { Mail *m = GetItem(); if (m) { LColourSelect *Colour; if (GetViewById(IDC_COLOUR, Colour)) { uint32_t Col = (uint32_t)Colour->Value(); m->SetMarkColour(Col); } else LgiTrace("%s:%i - Can't find IDC_COLOUR\n", _FL); auto s = GetCtrlName(IDC_LABEL); m->SetLabel(s); if (m->GetObject()->GetInt(FIELD_STORE_TYPE) != Store3Imap) // Imap knows to save itself. m->SetDirty(); m->Update(); } MetaFieldsDirty = false; } return b; } #define IDM_FILTER_BASE 2000 #define IDM_CHARSET_BASE 3000 void AddActions(LSubMenu *Menu, List &Filters, LArray Folders) { if (!Menu) return; auto StartLen = Menu->Length(); for (auto Folder: Folders) { for (ScribeFolder *f=Folder->GetChildFolder(); f; f=f->GetNextFolder()) { auto Sub = Menu->AppendSub(f->GetName(true)); if (Sub) { auto Item = Sub->GetParent(); if (Item) Item->Icon(ICON_CLOSED_FOLDER); Sub->SetImageList(Menu->GetImageList(), false); f->LoadThings(); AddActions(Sub, Filters, {f}); } } List a; Folder->LoadThings(); for (auto t: Folder->Items) { a.Insert(t->IsFilter()); } a.Sort(FilterCompare); for (auto i: a) { LVariant Name; if (i->GetVariant("Name", Name) && Name.Str()) { char n[256]; strcpy_s(n, sizeof(n), Name.Str()); for (char *s=n; *s; s++) { if (*s == '&') { memmove(s + 1, s, strlen(s)+1); s++; } } auto Item = Menu->AppendItem(n, (int) (IDM_FILTER_BASE + Filters.Length()), true); if (Item) { Item->Icon(ICON_FILTER); Filters.Insert(i); } } } } if (StartLen == Menu->Length()) { char s[64]; sprintf_s(s, sizeof(s), "(%s)", LLoadString(IDS_EMPTY)); Menu->AppendItem(s, 0, false); } } bool MailUi::OnViewKey(LView *v, LKey &k) { if (k.Down() && k.CtrlCmd() && !k.Alt()) { switch (k.vkey) { case LK_RETURN: { PostEvent(M_COMMAND, IDM_SEND_MSG); return true; } case LK_UP: { SeekMsg(-1); return true; } case LK_DOWN: { SeekMsg(1); return true; } } switch (k.c16) { case 'p': case 'P': { App->ThingPrint(NULL, GetItem(), NULL, this); break; } case 'f': case 'F': { if (Tab->Value() == 0) { if (TextView) TextView->DoFind(NULL); } else if (Tab->Value() == 1) { if (HtmlView) HtmlView->DoFind(NULL); } break; } case 'r': case 'R': { OnCommand(IDM_REPLY, 0, NULL); return true; } case 'w': case 'W': { if (OnRequestClose(false)) Quit(); return true; } case 's': case 'S': { if (IsDirty()) { SetDirty(false); } return true; } case 't': case 'T': { Tab->Value(0); if (TextView) TextView->Focus(true); return true; } case 'h': case 'H': { Tab->Value(1); if (HtmlView) HtmlView->Focus(true); return true; } } } return ThingUi::OnViewKey(v, k); } void MailUi::SerializeText(bool FromCtrl) { if (GetItem() && TextView) { if (FromCtrl) { GetItem()->SetBody(TextView->Name()); } else { TextView->Name(GetItem()->GetBody()); } } } const char *FilePart(const char *Uri) { const char *Dir = Uri + strlen(Uri) - 1; while (Dir > Uri && Dir[-1] != '/' && Dir[-1] != '\\') { Dir--; } return Dir; } void MailUi::OnAttachmentsChange() { Attachments->ResizeColumnsToContent(); if (GetItem()) { TabAttachments->GetCss(true)->FontBold(GetItem()->Attachments.Length() > 0); TabAttachments->OnStyleChange(); } } bool MailUi::NeedsCapability(const char *Name, const char *Param) { if (!InThread()) { PostEvent(M_NEEDS_CAP, (LMessage::Param)NewStr(Name)); } else { if (!Name) return false; if (Caps.Find(Name)) return true; Caps.Add(Name, true); char msg[256]; LArray Actions; LAutoPtr Back; if (!_stricmp(Name, "RemoteContent")) { Actions.Add(LLoadString(IDS_ALWAYS_SHOW_REMOTE_CONTENT)); Actions.Add(LLoadString(IDS_SHOW_REMOTE_CONTENT)); Back.Reset(new LColour(L_LOW)); strcpy_s(msg, sizeof(msg), LLoadString ( IDS_REMOTE_CONTENT_MSG, "To protect your privacy Scribe has blocked the remote content in this message." )); } else { Actions.Add(LLoadString(IDS_INSTALL)); int ch = 0; for (auto k : Caps) ch += sprintf_s(msg+ch, sizeof(msg)-ch, "%s%s", ch?", ":"", k.key); ch += sprintf_s(msg+ch, sizeof(msg)-ch, " is required to display this content."); } if (!MissingCaps) { MissingCaps = new MissingCapsBar(this, &Caps, msg, App, Actions, Back); auto c = IterateViews(); auto Idx = c.IndexOf(GpgUi); AddView(MissingCaps, (int)Idx+1); AttachChildren(); OnPosChange(); } else { MissingCaps->SetMsg(msg); } } return true; } void MailUi::OnCloseInstaller() { if (MissingCaps) { DeleteObj(MissingCaps); PourAll(); } } void MailUi::OnInstall(LCapabilityTarget::CapsHash *Caps, bool Status) { } void MailUi::OnChildrenChanged(LViewI *Wnd, bool Attaching) { if (Wnd == (LViewI*)MissingCaps && !Attaching) { MissingCaps = NULL; PourAll(); } } LDocView *MailUi::GetDoc(const char *MimeType) { if (!MimeType) { MimeType = sTextPlain; LVariant v; if (App->GetOptions()->GetValue(OPT_DefaultAlternative, v) && v.CastInt32() > 0) MimeType = sTextHtml; } LAssert(MimeType != NULL); if (!_stricmp(MimeType, sTextPlain)) return TextView; else if (!_stricmp(MimeType, sTextHtml)) return HtmlView; else LAssert(!"Invalid mime type."); return NULL; } bool MailUi::SetDoc(LDocView *v, const char *MimeType) { if (!MimeType) { MimeType = sTextPlain; LVariant v; if (App->GetOptions()->GetValue(OPT_DefaultAlternative, v) && v.CastInt32() > 0) MimeType = sTextHtml; } LAssert(MimeType != NULL); if (!_stricmp(MimeType, sTextPlain)) { LTextView3 *Txt = static_cast(v); if (Txt) { if (Txt != TextView) { DeleteObj(TextView); TextView = Txt; if (TabText && !TextView->IsAttached()) TabText->Append(TextView); } TextLoaded = true; if (_Running) TabText->Select(); } else { LAssert(!"Invalid ctrl."); return false; } } else if (!_stricmp(MimeType, sTextHtml)) { LDocView *Html = dynamic_cast(v); if (Html) { if (Html != HtmlView) { DeleteObj(HtmlView); HtmlView = Html; LCapabilityClient *cc = dynamic_cast(v); if (cc) cc->Register(this); if (TabHtml && !HtmlView->IsAttached()) TabHtml->Append(HtmlView); } HtmlLoaded = true; if (_Running) TabHtml->Select(); } else { LAssert(!"Invalid ctrl."); return false; } } else { LAssert(!"Invalid mime type."); return false; } return true; } bool MailUi::IsWorking(int Set) { if (!GetItem()) return false; // Are any of the attachments busy doing something? LDataI *AttachPoint = GetItem() && Set >= 0 ? GetItem()->GetFileAttachPoint() : NULL; List Attachments; if (GetItem()->GetAttachments(&Attachments)) { // Attachment *Match = NULL; for (auto a: Attachments) { if (Set >= 0) { a->SetIsResizing(Set != 0); if (AttachPoint) { a->GetObject()->Save(AttachPoint); } } else if (a->GetIsResizing()) { return true; } } } // Check rich text control as well... auto Rte = dynamic_cast(HtmlView); if (Rte) { if (Rte->IsBusy()) { return true; } } return false; } class BusyPanel : public LPanel { public: BusyPanel() : LPanel("Working...", LSysFont->GetHeight() << 1) { LViewI *v; LCss::ColorDef Bk(LColour(255, 128, 0)); GetCss(true)->BackgroundColor(Bk); AddView(v = new LTextLabel(IDC_STATIC, 30, 5, -1, -1, "Still resizing images...")); v->GetCss(true)->BackgroundColor(Bk); AddView(v = new LButton(IDCANCEL, 200, 3, -1, -1, LLoadString(IDS_CANCEL))); v->GetCss(true)->NoPaintColor(Bk); } }; void MailUi::SetCmdAfterResize(int Cmd) { if (CmdAfterResize == 0) { CmdAfterResize = Cmd; if (!WorkingDlg) { WorkingDlg = new BusyPanel; if (WorkingDlg) { AddView(WorkingDlg, 1); AttachChildren(); OnPosChange(); } } } } bool MailUi::OnRequestClose(bool OsClose) { bool Working = IsWorking(); if (Working) { SetCmdAfterResize(IDM_SAVE_CLOSE); return false; } return ThingUi::OnRequestClose(OsClose); } void MailUi::OnChange() { if (!IsDirty()) OnLoad(); } struct MailUiNameAddr { LString Name, Addr; MailUiNameAddr(const char *name = 0, const char *addr = 0) { Name = name; Addr = addr; } }; void MailUi::OnLoad() { bool Edit = false; bool ReadOnly = true; Mail *Item = GetItem(); _Running = false; if (Item && Item->App) { Edit = TestFlag(Item->GetFlags(), MAIL_CREATED); ReadOnly = !TestFlag(Item->GetFlags(), MAIL_CREATED | MAIL_BOUNCE); if (Entry) { Entry->Name(""); } if (To) { To->OnInit(Item->GetTo()); } if (FromCbo) { LHashTbl, MailUiNameAddr*> ReplyToAddrs; const char *Template = "%s <%s>"; FromCbo->Empty(); int Idx = -1; LVariant DefName, DefAddr; // LOptionsFile *Opts = Item->Window->GetOptions(); for (auto a : *Item->App->GetAccounts()) { LVariant Name = a->Identity.Name(); LVariant Addr = a->Identity.Email(); LVariant ReplyTo = a->Identity.ReplyTo(); if (!a->IsValid() || a->Send.Disabled()) continue; if (ReplyTo.Str()) { if (!ReplyToAddrs.Find(ReplyTo.Str())) ReplyToAddrs.Add(ReplyTo.Str(), new MailUiNameAddr(Name.Str(), ReplyTo.Str())); } else if (Addr.Str()) { if (!ReplyToAddrs.Find(Addr.Str())) ReplyToAddrs.Add(Addr.Str(), new MailUiNameAddr(Name.Str(), Addr.Str())); } if (Name.Str() && Addr.Str()) { if (!DefAddr.Str() || _stricmp(DefAddr.Str(), Addr.Str())) { auto FromAddr = Item->GetFromStr(FIELD_EMAIL); if (FromAddr && _stricmp(Addr.Str(), FromAddr) == 0) { Idx = (int)FromCbo->Length(); } LString p; p.Printf(Template, Name.Str(), Addr.Str()); int Id = a->Receive.Id(); int CurLen = (int)FromCbo->Length(); LAssert(Id != 0); FromAccountId[CurLen] = Id; FromCbo->Insert(p); } } } if (Idx < 0) { auto FromName = Item->GetFromStr(FIELD_NAME); auto FromAddr = Item->GetFromStr(FIELD_EMAIL); if (FromAddr) { LStringPipe p; if (FromName) p.Print(Template, FromName, FromAddr); else p.Print("<%s>", FromAddr); LAutoString s(p.NewStr()); FromAccountId[FromCbo->Length()] = -1; Idx = (int)FromCbo->Length(); FromCbo->Insert(s); FromCbo->Value(Idx); } } else { FromCbo->Value(Idx); } if (ReplyToAddrs.Length() > 0 && ReplyToCbo) { auto CurAddr = Item->GetReply() ? Item->GetReply()->GetStr(FIELD_EMAIL) : NULL; int CurIdx = -1; // for (MailUiNameAddr *na = ReplyToAddrs.First(); na; na = ReplyToAddrs.Next()) for (auto na : ReplyToAddrs) { char s[256]; sprintf_s(s, sizeof(s), Template, na.value->Name.Get(), na.value->Addr.Get()); if (CurAddr && !_stricmp(na.value->Addr, CurAddr)) CurIdx = (int)ReplyToCbo->Length(); ReplyToCbo->Insert(s); } if (CurIdx >= 0) { ReplyToCbo->Value(CurIdx); ReplyToChk->Value(true); } else { ReplyToChk->Value(false); } } ReplyToAddrs.DeleteObjects(); } else if (FromList) { FromList->Empty(); ListAddr *na = new ListAddr(App, Item->GetFrom()); if (na) { na->CC = MAIL_ADDR_FROM; na->OnFind(); FromList->Insert(na); } } if (Subject) { Subject->Name(Item->GetSubject()); char Title[140]; auto Subj = Item->GetSubject(); if (Subj) sprintf_s(Title, sizeof(Title), "%s - %.100s", LLoadString(IDS_MAIL_MESSAGE), Subj); else sprintf_s(Title, sizeof(Title), "%s", LLoadString(IDS_MAIL_MESSAGE)); Title[sizeof(Title)-1] = 0; for (char *s = Title; *s; s++) if (*s == '\n' || *s == '\r') *s = ' '; Name(Title); } int64_t Rgb32 = Item->GetMarkColour(); SetCtrlValue(IDC_COLOUR, Rgb32 > 0 ? Rgb32 : -1); SetCtrlName(IDC_LABEL, Item->GetLabel()); char Date[256]; Item->GetDateReceived()->Get(Date, sizeof(Date)); SetCtrlName(IDC_RECEIVED_DATE, Date); Item->GetDateSent()->Get(Date, sizeof(Date)); SetCtrlName(IDC_SENT_DATE, Date); TextLoaded = false; HtmlLoaded = false; auto TextContent = Item->GetBody(); auto HtmlContent = Item->GetHtml(); Sx = Sy = -1; LDocView *DocView = NULL; if (TabText && (DocView = Item->CreateView(this, sTextPlain, true, -1, !Edit))) { if (DocView == HtmlView) { // CreateView converted the text to HTML to embed Emojis. If we have // actual HTML content it'll overwrite the text portion, so we need // to move the HTML control to the text tab to leave room for actual HTML. DeleteObj(TextView); TextView = HtmlView; HtmlView = NULL; TextView->Detach(); TabText->Append(TextView); TextLoaded = true; HtmlLoaded = false; } if (!TextView && Edit) { // What the? Force creation of control... LAssert(!"Must have an edit control."); LDocView *Dv = App->CreateTextControl(IDC_TEXT_VIEW, sTextPlain, Edit, GetItem()); if (Dv) SetDoc(Dv, sTextPlain); LAssert(TextView != NULL); } if (TextView) { TextView->Visible(true); // This needs to be below the resize of the control so that // any wrapping has already been done and thus the scroll // bar is laid out already. if (Item->Cursor > 0) TextView->SetCaret(Item->Cursor, false); TabText->GetCss(true)->FontBold(ValidStr(TextContent)); TabText->OnStyleChange(); } } bool ValidHtml = ValidStr(HtmlContent); if (TabHtml && Item->CreateView(this, sTextHtml, true, -1, !Edit) && HtmlView) { HtmlView->Visible(true); if (Item->Cursor > 0) HtmlView->SetCaret(Item->Cursor, false); TabHtml->GetCss(true)->FontBold(ValidHtml); TabHtml->OnStyleChange(); } LVariant DefTab; Item->App->GetOptions()->GetValue(Edit ? OPT_EditControl : OPT_DefaultAlternative, DefTab); CurrentEditCtrl = (Edit || ValidHtml) && DefTab.CastInt32(); Tab->Value(CurrentEditCtrl); HtmlCtrlDirty = !ValidHtml; TextCtrlDirty = !ValidStr(TextContent); if (CalendarPanel) { auto CalEvents = GetItem()->GetCalendarAttachments(); CalendarPanel->Open(CalEvents.Length() > 0); } OnPosChange(); if (Attachments) { Attachments->RemoveAll(); List Files; if (Item->GetAttachments(&Files)) { for (auto a: Files) Attachments->Insert(a); } OnAttachmentsChange(); } if (Header) { LAutoString Utf((char*)LNewConvertCp("utf-8", Item->GetInternetHeader(), "iso-8859-1")); Header->Name(Utf); Header->SetEnv(Item); } bool Update = (Item->GetFlags() & MAIL_READ) == 0 && (Item->GetFlags() & MAIL_CREATED) == 0; if (Update) { Item->SetFlags(Item->GetFlags() | MAIL_READ); } if (Commands.Toolbar) { int p = Item->GetPriority(); Commands.Toolbar->SetCtrlValue(IDM_HIGH_PRIORITY, p < MAIL_PRIORITY_NORMAL); Commands.Toolbar->SetCtrlValue(IDM_LOW_PRIORITY, p > MAIL_PRIORITY_NORMAL); Commands.Toolbar->SetCtrlValue(IDM_READ_RECEIPT, TestFlag(Item->GetFlags(), MAIL_READ_RECEIPT)); } } if (Item->GetFlags() & (MAIL_CREATED | MAIL_BOUNCE)) { if (Entry) Entry->Focus(true); } else { if (TextView) TextView->Focus(true); } if (BtnPrev && BtnNext) { if (Item && Container) { /* int Items = Item->GetList()->Length(); int i = Item->GetList()->IndexOf(Item); */ auto Items = Container->Length(); auto i = Container->IndexOf(Item); BtnPrev->Enabled(i < (ssize_t)Items - 1); BtnNext->Enabled(i > 0); } else { BtnPrev->Enabled(false); BtnNext->Enabled(false); } } if (BtnSend) BtnSend->Enabled(!ReadOnly); if (BtnSave) BtnSave->Enabled(!ReadOnly); if (BtnSaveClose) BtnSaveClose->Enabled(!ReadOnly); if (BtnAttach) BtnAttach->Enabled(!ReadOnly); if (BtnReply) BtnReply->Enabled(ReadOnly); if (BtnReplyAll) BtnReplyAll->Enabled(ReadOnly); if (BtnForward) BtnForward->Enabled(true); if (BtnBounce) BtnBounce->Enabled(ReadOnly); if (Commands.Toolbar) { Commands.Toolbar->SetCtrlEnabled(IDM_HIGH_PRIORITY, Edit); Commands.Toolbar->SetCtrlEnabled(IDM_LOW_PRIORITY, Edit); Commands.Toolbar->SetCtrlEnabled(IDM_READ_RECEIPT, Edit); } _Running = true; } void MailUi::OnSave() { if (!GetItem()) return; if (GetItem()->GetFlags() & MAIL_SENT) { // Save a copy instead of over writing the original sent email Mail *Copy = new Mail(App, GetItem()->GetObject()->GetStore()->Create(MAGIC_MAIL)); if (Copy) { *Copy = *_Item; Copy->SetFlags(MAIL_READ | MAIL_CREATED, true); Copy->SetFolder(GetItem()->GetFolder()); Copy->SetDateSent(0); SetItem(Copy); } } Mail *Item = GetItem(); if (To) { To->OnSave(Item->GetObject()->GetStore(), Item->GetTo()); } LMailStore *AccountMailStore = NULL; if (FromCbo && Item->GetFrom() && FromAccountId.Length() > 0) { int64 CboVal = FromCbo->Value(); LAssert(CboVal < (ssize_t)FromAccountId.Length()); int AccountId = FromAccountId[(int)CboVal]; LDataPropI *Frm = Item->GetFrom(); if (AccountId < 0) { // From is a literal address, not an account ID. This can happen when bouncing email. Mailto mt(App, FromCbo->Name()); if (mt.To.Length() == 1) { AddressDescriptor *a = mt.To[0]; if (a) { Frm->SetStr(FIELD_NAME, a->sName); Frm->SetStr(FIELD_EMAIL, a->sAddr); } } else LAssert(0); } else if (AccountId > 0) { ScribeAccount *a = Item->App->GetAccountById(AccountId); if (a) { Frm->SetStr(FIELD_NAME, a->Identity.Name().Str()); Frm->SetStr(FIELD_EMAIL, a->Identity.Email().Str()); } else LAssert(!"From account missing."); // Find the associated mail store for this account. Hopefully we can put any new // mail into the mail store that the account is using. // // Check for IMAP mail store? AccountMailStore = a->Receive.GetMailStore(); if (!AccountMailStore) { // Nope... what about a receive path? LVariant DestFolder = a->Receive.DestinationFolder(); if (ValidStr(DestFolder.Str())) { AccountMailStore = Item->App->GetMailStoreForPath(DestFolder.Str()); } } } else LAssert(!"No account id."); } LDataPropI *ReplyObj = Item->GetReply(); if (ReplyToCbo != NULL && ReplyObj != NULL && ReplyToChk != NULL && ReplyToChk->Value()) { Mailto mt(App, ReplyToCbo->Name()); if (mt.To.Length() == 1) { AddressDescriptor *a = mt.To[0]; if (a && a->sAddr) { if (a->sName) ReplyObj->SetStr(FIELD_NAME, a->sName); ReplyObj->SetStr(FIELD_EMAIL, a->sAddr); } } } Item->SetSubject(Subject->Name()); Item->SetLabel(GetCtrlName(IDC_LABEL)); auto c32 = GetCtrlValue(IDC_COLOUR); Item->SetMarkColour(c32); LDocView *Ctrl = CurrentEditCtrl ? HtmlView : TextView; if (Ctrl) { // Delete all existing data... Item->SetBody(0); Item->SetBodyCharset(0); Item->SetHtml(0); Item->SetHtmlCharset(0); const char *MimeType = Ctrl->GetMimeType(); const char *Charset = Ctrl->GetCharset(); if (!_stricmp(MimeType, sTextHtml)) { LArray Media; // Set the HTML part LString HtmlFormat; if (!Ctrl->GetFormattedContent("text/html", HtmlFormat, &Media)) HtmlFormat = Ctrl->Name(); Item->SetHtml(HtmlFormat); Item->SetHtmlCharset(Charset); // Also set a text version for the alternate LString TxtFormat; if (!Ctrl->GetFormattedContent(sTextPlain, TxtFormat)) { TxtFormat = HtmlToText(Item->GetHtml(), Charset); } if (TxtFormat) { Item->SetBody(TxtFormat); Item->SetBodyCharset(Charset); } auto Obj = Item->GetObject(); // This clears any existing multipart/related objects... Obj->SetObj(FIELD_HTML_RELATED, NULL); if (Media.Length() > 0) { // Make a table of existing attachments so that we don't duplicate // these new ones. LArray Objs; LHashTbl,LDataI*> Map; if (GetItem()->GetAttachmentObjs(Objs)) { for (auto i : Objs) { auto Cid = i->GetStr(FIELD_CONTENT_ID); if (Cid) Map.Add(Cid, i); } } // If there are media attachments, splice them into the MIME tree. // This should go after setting the text part so that the right // MIME alternative structure is generated. auto Store = Obj->GetStore(); for (auto &Cm : Media) { LDataI *a = Store->Create(MAGIC_ATTACHMENT); if (a) { LAssert(Cm.Valid()); auto Existing = Map.Find(Cm.Id); if (Existing) { // Delete the existing attachment Thing *t = CastThing(Existing); Attachment *a = t ? t->IsAttachment() : NULL; if (a) { // Delete both the Attachment and it's store object... auto it = GetItem(); it->DeleteAttachment(a); } else { // There is Attachment object for the LDataI.... but we can // still delete it from the store. LArray del; del.Add(Existing); Existing->GetStore()->Delete(del, false); } } LgiTrace("Adding related: %s %s " LPrintfInt64 "\n", Cm.FileName.Get(), Cm.MimeType.Get(), Cm.Stream->GetSize()); a->SetStr(FIELD_CONTENT_ID, Cm.Id); a->SetStr(FIELD_NAME, Cm.FileName); a->SetStr(FIELD_MIME_TYPE, Cm.MimeType); a->SetStream(Cm.Stream); Obj->SetObj(FIELD_HTML_RELATED, a); } } } } else { auto Text = Ctrl->Name(); Item->SetBody(Text); Item->SetBodyCharset(Charset); } } #if SAVE_HEADERS char *Headers = Header ? Header->Name() : 0; if (Headers) { DeleteArray(Item->InternetHeader); Item->InternetHeader = NewStr(Headers); } #endif Item->GetMessageId(true); Item->CreateMailHeaders(); Item->Update(); ScribeFolder *Folder = Item->GetFolder(); // Now get the associated outbox for this mail ScribeFolder *Outbox = Item->App->GetFolder(FOLDER_OUTBOX, AccountMailStore); auto Fld = Folder ? Folder : Outbox; LAssert(Fld != NULL); bool Status = Fld ? Item->Save(Fld) : false; if (Status) { LArray c; c.Add(Item->GetObject()); Item->App->SetContext(_FL); Item->App->OnChange(c, 0); } } bool MailUi::AddRecipient(AddressDescriptor *Addr) { if (Addr && To) { ListAddr *La = dynamic_cast(Addr); if (La) { To->Insert(La); return true; } } return false; } bool MailUi::AddRecipient(Contact *c) { ListAddr *La = new ListAddr(c); if (La) { To->Insert(La); return true; } return false; } bool MailUi::AddRecipient(const char *Email, const char *Name) { ListAddr *La = new ListAddr(App, Email, Name); if (La) { To->Insert(La); return true; } return false; } bool MailUi::SeekMsg(int delta) { bool Status = false; if (Container) { Mail *Item = GetItem(); auto Index = Container->IndexOf(Item); Mail *Next = Index >= 0 ? (*Container)[Index + delta] : 0; if (Next) { SetDirty(false); // called OnSave() if necessary _Running = false; if (Item->Ui) { // close any existing user interface if (Item->Ui != this) { Item->Ui->Quit(); } else { Item->Ui = 0; } } if (Header) Header->SetEnv(0); Caps.Empty(); SetItem(Next); Item = GetItem(); // select this item in the list if (Item->GetList()) { Item->GetList()->Select(Item); Item->ScrollTo(); } Status = true; OnLoad(); _Running = true; } } return Status; } int MailUi::HandleCmd(int Cmd) { switch (Cmd) { case IDM_READ_RECEIPT: { if (Commands.Toolbar) { int f = GetItem()->GetFlags(); if (Commands.Toolbar->GetCtrlValue(IDM_READ_RECEIPT)) { SetFlag(f, MAIL_READ_RECEIPT); } else { ClearFlag(f, MAIL_READ_RECEIPT); } GetItem()->SetFlags(f); } break; } case IDM_HIGH_PRIORITY: { if (Commands.Toolbar) { GetItem()->SetPriority(Commands.Toolbar->GetCtrlValue(IDM_HIGH_PRIORITY) ? MAIL_PRIORITY_HIGH : MAIL_PRIORITY_NORMAL); SetDirty(true); Commands.Toolbar->SetCtrlValue(IDM_HIGH_PRIORITY, GetItem()->GetPriority() < MAIL_PRIORITY_NORMAL); Commands.Toolbar->SetCtrlValue(IDM_LOW_PRIORITY, GetItem()->GetPriority() > MAIL_PRIORITY_NORMAL); } break; } case IDM_LOW_PRIORITY: { if (Commands.Toolbar) { GetItem()->SetPriority(Commands.Toolbar->GetCtrlValue(IDM_LOW_PRIORITY) ? MAIL_PRIORITY_LOW : MAIL_PRIORITY_NORMAL); SetDirty(true); Commands.Toolbar->SetCtrlValue(IDM_HIGH_PRIORITY, GetItem()->GetPriority() < MAIL_PRIORITY_NORMAL); Commands.Toolbar->SetCtrlValue(IDM_LOW_PRIORITY, GetItem()->GetPriority() > MAIL_PRIORITY_NORMAL); } break; } case IDM_PREV_MSG: { SeekMsg(1); break; } case IDM_NEXT_MSG: { SeekMsg(-1); break; } case IDM_SEND_MSG: { bool Working = IsWorking(); if (Working) { SetCmdAfterResize(Cmd); break; } if (!GetItem() || !App) { LAssert(!"Missing item or window ptr."); break; } // Normal save bool IsInPublicFolder = GetItem()->GetFolder() && GetItem()->GetFolder()->IsPublicFolders(); if (IsInPublicFolder) { auto i = GetItem(); LDateTime n; n.SetNow(); i->SetDateSent(&n); i->Update(); } OnDataEntered(); OnSave(); SetDirty(false, false); GetItem()->Send(true); Quit(); return 0; } case IDM_DELETE_MSG: { LVariant ConfirmDelete, DelDirection; App->GetOptions()->GetValue(OPT_ConfirmDelete, ConfirmDelete); App->GetOptions()->GetValue(OPT_DelDirection, DelDirection); int WinAction = DelDirection.CastInt32() - 1; // -1 == Next, 0 == Close, 1 == Prev if (!ConfirmDelete.CastInt32() || LgiMsg(this, LLoadString(IDS_DELETE_ASK), AppName, MB_YESNO) == IDYES) { Mail *Del = GetItem()->GetObject() ? GetItem() : 0; if (Del) { if (!Del->GetObject()->IsOnDisk()) Del = 0; } if (!WinAction || !SeekMsg(WinAction)) { SetItem(0); PostEvent(M_CLOSE); } if (Del && Del->GetObject()) { Del->OnDelete(); } } break; } case IDM_DELETE_AS_SPAM: { LVariant DelDirection; App->GetOptions()->GetValue(OPT_DelDirection, DelDirection); int WinAction = DelDirection.CastInt32() - 1; // -1 == Next, 0 == Close, 1 == Prev if (GetItem()) { Mail *Del = GetItem()->GetObject() ? GetItem() : 0; if (!WinAction || !SeekMsg(WinAction)) { SetItem(0); PostEvent(M_CLOSE); } if (Del) { Del->DeleteAsSpam(this); } } break; } case IDM_SAVE: { OnDataEntered(); SetDirty(false, false); break; } case IDM_SAVE_CLOSE: { bool Working = IsWorking(); if (Working) { SetCmdAfterResize(Cmd); break; } OnDataEntered(); SetDirty(false, false); // fall thru } case IDM_CLOSE: { Quit(); return 0; } case IDM_REPLY: case IDM_REPLY_ALL: { App->MailReplyTo(GetItem(), Cmd == IDM_REPLY_ALL); SetDirty(false); PostEvent(M_CLOSE); break; } case IDM_FORWARD: { App->MailForward(GetItem()); if (IsDirty()) OnSave(); PostEvent(M_CLOSE); break; } case IDM_BOUNCE: { App->MailBounce(GetItem()); OnSave(); PostEvent(M_CLOSE); break; } case IDM_PRINT: { if (GetItem() && App) { if (IsDirty()) { OnDataEntered(); OnSave(); } App->ThingPrint(NULL, GetItem(), 0, this); } break; } case IDM_ATTACH_FILE: { auto Select = new LFileSelect(this); Select->MultiSelect(true); Select->Type("All files", LGI_ALL_FILES); Select->Open([this](auto dlg, auto status) { if (status) { Mail *m = GetItem(); if (m) { for (size_t i=0; iLength(); i++) { char File[MAX_PATH_LEN]; if (!LResolveShortcut((*dlg)[i], File, sizeof(File))) { strcpy_s(File, sizeof(File), (*dlg)[i]); } Attachment *a = m->AttachFile(this, File); if (a && Attachments) { Attachments->Insert(a); Attachments->ResizeColumnsToContent(); } } } } delete dlg; }); break; } default: { if (Commands.ExecuteCallbacks(App, this, GetItem(), Cmd)) return true; return false; } } return true; } int MailUi::OnCommand(int Cmd, int Event, OsView From) { if (GpgUi) { GpgUi->DoCommand(Cmd, [this, Cmd](auto r) { if (!r) HandleCmd(Cmd); }); } else { HandleCmd(Cmd); } return LWindow::OnCommand(Cmd, Event, From); } LArray Mail::GetCalendarAttachments() { List Attachments; if (!GetAttachments(&Attachments)) return false; LArray Cal; for (auto a: Attachments) { LString Mt = a->GetMimeType(); if (Mt.Equals("text/calendar")) Cal.Add(a); } return Cal; } bool MailUi::AddCalendarEvent(bool AddPopupReminder) { LString Msg; auto Result = GetItem()->AddCalendarEvent(this, AddPopupReminder, &Msg); auto css = CalPanelStatus->GetCss(true); if (Result) css->Color(LCss::ColorInherit); else css->Color(LColour::Red); CalPanelStatus->Name(Msg); return Result; } bool Mail::AddCalendarEvent(LViewI *Parent, bool AddPopupReminder, LString *Msg) { LString Err, s; auto Cal = GetCalendarAttachments(); int NewEvents = 0, DupeEvents = 0, Processed = 0, Cancelled = 0, NotMatched = 0; ScribeFolder *Folder = NULL; if (Cal.Length() == 0) { Err = "There are no attached events to add."; goto OnError; } Folder = App->GetFolder(FOLDER_CALENDAR); if (!Folder) { Err = "There no calendar folder to save to."; goto OnError; } for (auto a: Cal) { auto Event = App->CreateThingOfType(MAGIC_CALENDAR); if (Event) { LString Mt = a->GetMimeType(); LAutoPtr Data(a->GotoObject(_FL)); if (Data) { if (Event->Import(AutoCast(Data), Mt)) { auto c = Event->IsCalendar(); auto obj = c ? c->GetObject() : NULL; if (!obj) continue; if (AddPopupReminder) { LString s; s.Printf("%g,%i,%i,", 10.0, CalMinutes, CalPopup); obj->SetStr(FIELD_CAL_REMINDERS, s); } // Is it a cancellation? auto Status = obj->GetStr(FIELD_CAL_STATUS); auto IsCancel = Stristr(Status, "CANCELLED") != NULL; if (!IsCancel) { // Does the folder already have a copy of this event? bool AlreadyAdded = false; for (auto t: Folder->Items) { auto Obj = t->IsCalendar(); if (Obj && *Obj == *c) { AlreadyAdded = true; break; } } if (AlreadyAdded) { DupeEvents++; } else { // Write the event to the folder auto Status = Folder->WriteThing(Event); if (Status > Store3Error) { NewEvents++; Event = NULL; } } } else { // Cancellation processing auto Uid = obj->GetStr(FIELD_UID); Thing *Match = NULL; for (auto t: Folder->Items) { auto tCal = t->IsCalendar(); if (tCal && tCal->GetObject()) { auto tUid = tCal->GetObject()->GetStr(FIELD_UID); if (!Stricmp(Uid, tUid)) { Match = t; break; } } } if (Match) { if (!Parent || LgiMsg(Parent, "Delete cancelled event?", "Calendar", MB_YESNO) == IDYES) { auto f = Match->GetFolder(); LArray items; items.Add(Match); f->Delete(items, true); Cancelled++; } } else NotMatched++; } } else LgiTrace("%s:%i - vCal event import failed.\n", _FL); } else LgiTrace("%s:%i - GotoObject failed.\n", _FL); if (Event) Event->DecRef(); } else LgiTrace("%s:%i - CreateThingOfType failed.\n", _FL); } Processed = NewEvents + DupeEvents; if (Processed != Cal.Length()) { Err.Printf("There were errors processing %i events, check the console.", (int)Cal.Length() - Processed); goto OnError; } if (NewEvents || DupeEvents) s.Printf("%i new events, %i duplicates.", NewEvents, DupeEvents); else s.Printf("%i events cancelled, %i not matched.", Cancelled, NotMatched); if (Msg) *Msg = s; if (Processed > 0) { for (auto v: CalendarView::CalendarViews) v->OnContentsChanged(); } return true; OnError: if (Msg) *Msg = Err; return false; } int MailUi::OnNotify(LViewI *Col, LNotification n) { if (dynamic_cast(Col)) { Sx = Sy = -1; OnPosChange(); return 0; } if (GpgUi) { if (n.Type == LNotifyItemDelete && Col == (LViewI*)GpgUi) { GpgUi = NULL; } else { int r = GpgUi->OnNotify(Col, n); if (r) return r; } } int CtrlId = Col->GetId(); switch (CtrlId) { case IDC_MAIL_UI_TABS: { Mail *Item = GetItem(); if (!Item) break; // bool Edit = TestFlag(Item->GetFlags(), MAIL_CREATED); if (n.Type == LNotifyValueChanged) { switch (Col->Value()) { case 0: // Text tab { if (!TextLoaded) { Item->CreateView(this, sTextPlain, true, -1); OnPosChange(); } if (CurrentEditCtrl == 1) { if (HtmlView && TextView && TextCtrlDirty) { // Convert HTML to Text here... TextCtrlDirty = false; auto Html = HtmlView->Name(); if (Html) { LString Txt = HtmlToText(Html, HtmlView->GetCharset()); if (Txt) TextView->Name(Txt); } } CurrentEditCtrl = 0; } break; } case 1: // Html tab { if (!HtmlLoaded) { Item->CreateView(this, sTextHtml, true, -1); OnPosChange(); } if (CurrentEditCtrl == 0) { if (HtmlView && TextView && HtmlCtrlDirty) { // Convert Text to HTML here... HtmlCtrlDirty = false; auto Text = TextView->Name(); if (Text) { LString Html = TextToHtml(Text, TextView->GetCharset()); if (Html) HtmlView->Name(Html); } } CurrentEditCtrl = 1; } break; } default: // Do nothing on other tabs.. break; } } else if (n.Type == LNotifyItemClick) { LMouse m; if (!Col->GetMouse(m)) break; int TabIdx = Tab->HitTest(m); if (TabIdx == 0 || TabIdx == 1) { if (Item && m.IsContextMenu()) { LSubMenu s; s.AppendItem(LLoadString(IDS_DELETE), IDM_DELETE); m.ToScreen(); int Cmd = s.Float(this, m.x, m.y, false); if (Cmd == IDM_DELETE) { if (TabIdx == 0) // Txt { Item->SetBody(NULL); Item->SetBodyCharset(NULL); TextView->Name(NULL); if (TabText) { TabText->GetCss(true)->FontBold(false); TabText->OnStyleChange(); } TextCtrlDirty = false; } else // HTML { Item->SetHtml(NULL); Item->SetHtmlCharset(NULL); HtmlView->Name(NULL); if (TabHtml) { TabHtml->GetCss(true)->FontBold(false); TabHtml->OnStyleChange(); } HtmlCtrlDirty = false; } } } } } break; } case IDC_FROM: { if (_Running && FromCbo && n.Type == LNotifyValueChanged) SetDirty(true); break; } case IDC_SHOW_FROM: { LVariant Show = Col->Value();; App->GetOptions()->SetValue(OPT_MailShowFrom, Show); break; } case IDC_LAUNCH_HTML: { char File[MAX_PATH_LEN]; if (GetItem() && GetItem()->WriteAlternateHtml(File)) { LExecute(File); } break; } case IDC_ENTRY: { if (Entry) { if (ValidStr(Entry->Name())) { if (!Browse) { Browse = new AddressBrowse(App, Entry, To, SetTo); } } if (Browse) { Browse->OnNotify(Entry, n); } if (n.Type == LNotifyReturnKey) { OnDataEntered(); } } break; } case IDC_SET_TO: { if (SetTo) { AddMode = (int)SetTo->Value(); } break; } case IDC_SEND: { OnCommand(IDM_SEND_MSG, 0, #if LGI_VIEW_HANDLE Col->Handle() #else (OsView)NULL #endif ); break; } #if SAVE_HEADERS case IDC_INTERNET_HEADER: #endif case IDC_TEXT_VIEW: case IDC_HTML_VIEW: { Mail *Item = GetItem(); if (!Item) break; bool Edit = TestFlag(Item->GetFlags(), MAIL_CREATED); if ( ( n.Type == LNotifyDocChanged || n.Type == LNotifyCharsetChanged || n.Type == LNotifyFixedWidthChanged || (!IgnoreShowImgNotify && n.Type == LNotifyShowImagesChanged) ) && _Running ) { if (GetItem()) GetItem()->OnNotify(Col, n); SetDirty(true); if (Edit) { if (CtrlId == IDC_TEXT_VIEW) { CurrentEditCtrl = 0; HtmlCtrlDirty = true; TabText->GetCss(true)->FontBold(true); TabText->OnStyleChange(); } else if (CtrlId == IDC_HTML_VIEW) { CurrentEditCtrl = 1; TextCtrlDirty = true; TabHtml->GetCss(true)->FontBold(true); TabHtml->OnStyleChange(); } // LgiTrace("%s:%i - OnNotify: TextLoaded=%i, HtmlLoaded=%i\n", _FL, TextLoaded, HtmlLoaded); } } break; } case IDC_ATTACHMENTS: { if (n.Type == LNotifyItemInsert || n.Type == LNotifyItemDelete) { // fall thru } else { break; } } case IDC_TO: { if ( _Running && ( n.Type == LNotifyItemInsert || n.Type == LNotifyItemDelete || n.Type == LNotifyItemChange ) ) { SetDirty(true); } break; } case IDC_COLOUR: { if (_Running && GetItem()) { MetaFieldsDirty = true; OnDirty(true); } break; } case IDC_LABEL: { if (_Running) { MetaFieldsDirty = true; OnDirty(true); } break; } case IDC_SUBJECT: { if (_Running) { SetDirty(true); } break; } case IDCANCEL: { if (CmdAfterResize) { int Cmd = CmdAfterResize; CmdAfterResize = 0; IsWorking(false); OnCommand(Cmd, 0, NULL); } break; } case IDC_ADD_CAL_EVENT: { AddCalendarEvent(false); break; } case IDC_ADD_CAL_EVENT_POPUP: { AddCalendarEvent(true); break; } } return 0; } void MailUi::OnDataEntered() { auto Name = Entry->Name(); if (ValidStr(Name)) { List New; // Decode the entries Mailto mt(App, Name); New = mt.To; mt.To.Empty(); if (mt.Subject && !ValidStr(GetCtrlName(IDC_SUBJECT))) { SetCtrlName(IDC_SUBJECT, mt.Subject); } // Add the new entries List Cache; App->GetContacts(Cache); AddressDescriptor *ad; while ((ad = New[0])) { New.Delete(ad); ListAddr *t = dynamic_cast(ad); if (t) { t->CC = (EmailAddressType)GetCtrlValue(IDC_SET_TO); t->OnFind(&Cache); To->Insert(t, 0); } else { DeleteObj(ad); } } // Clear the entry box for the next one Entry->Name(""); Entry->Select(-1, -1); } } void MailUi::OnPosChange() { LWindow::OnPosChange(); if (Tab && (Sx != X() || Sy != Y())) { Sx = X(); Sy = Y(); LRect r = Tab->GetCurrent()->GetClient(); r.Inset(CONTENT_BORDER, CONTENT_BORDER); if (TextView) TextView->SetPos(r, true); if (HtmlView) HtmlView->SetPos(r, true); if (Attachments) Attachments->SetPos(r, true); if (Header) Header->SetPos(r, true); } } void MailUi::OnPaint(LSurface *pDC) { LCssTools Tools(this); Tools.PaintContent(pDC, GetClient()); } void MailUi::OnPulse() { if (IsDirty() && TextView && GetItem()) { // Ui -> Object OnSave(); // Object -> Disk GetItem()->Save(0); } else { SetPulse(); } } void MailUi::OnDirty(bool Dirty) { SetCtrlEnabled(IDM_SAVE, Dirty); SetCtrlEnabled(IDM_SAVE_CLOSE, Dirty); if (Dirty) { SetPulse(60 * 1000); // every minute } else { SetPulse(); } } bool MailUi::CallMethod(const char *Name, LVariant *Dst, LArray &Arg) { ScribeDomType Method = StrToDom(Name); *Dst = false; switch (Method) { case SdShowRemoteContent: // Type: () if (HtmlView) { bool Always = Arg.Length() > 0 ? Arg[0]->CastBool() : false; if (Always && GetItem()) { auto From = GetItem()->GetFrom(); if (From) App->RemoteContent_AddSender(From->GetStr(FIELD_EMAIL), true); else LgiTrace("%s:%i - No from address.\n", _FL); } IgnoreShowImgNotify = true; HtmlView->SetLoadImages(true); IgnoreShowImgNotify = false; PostEvent(M_UPDATE); *Dst = true; } break; case SdSetHtml: // Type: (String Html) if (HtmlView) { if (Arg.Length() > 0) { HtmlView->Name(Arg[0]->Str()); *Dst = true; } } break; default: return false; } return true; } LMessage::Result MailUi::OnEvent(LMessage *Msg) { switch (Msg->Msg()) { case M_SET_HTML: { LAutoPtr s((LString*)Msg->A()); if (s && HtmlView) HtmlView->Name(*s); break; } case M_NEEDS_CAP: { LAutoString c((char*)Msg->A()); NeedsCapability(c); return 0; } case M_RESIZE_IMAGE: { LAutoPtr Job((ImageResizeThread::Job*)Msg->A()); if (Job && GetItem()) { // Find the right attachment... List Attachments; if (GetItem()->GetAttachments(&Attachments)) { Attachment *Match = NULL; for (auto a: Attachments) { LString Nm = a->GetName(); if (Nm.Equals(Job->FileName)) { Match = a; break; } } if (Match) { if (Job->Data) Match->Set(Job->Data); auto Mt = Match->GetMimeType(); if (_stricmp(Mt, "image/jpeg")) { auto Name = Match->GetName(); char *Ext = LGetExtension(Name); if (Ext) *Ext = 0; LString NewName = Name; NewName += "jpg"; Match->SetName(NewName); Match->SetMimeType("image/jpeg"); } Match->SetIsResizing(false); LDataI *AttachPoint = GetItem()->GetFileAttachPoint(); if (AttachPoint) { // Do final save to the mail store... Match->GetObject()->Save(AttachPoint); } else { LAssert(0); Match->DecRef(); Match = NULL; } if (CmdAfterResize) { bool Working = IsWorking(); if (!Working) { // All resizing work is done... OnCommand(CmdAfterResize, 0, NULL); return 0; } } } else LAssert(!"No matching attachment image to store resized image in."); } } break; } #if WINNATIVE case WM_COMMAND: { LAssert((NativeInt)Commands.Toolbar != 0xdddddddd); if (Commands.Toolbar && Commands.Toolbar->Handle() == (HWND)Msg->b) { return LWindow::OnEvent(Msg); } break; } #endif } return LWindow::OnEvent(Msg); } void MailUi::OnReceiveFiles(LArray &Files) { List Att; GetItem()->GetAttachments(&Att); for (unsigned i=0; iGetName(), f) == 0) { int Result = LgiMsg(this, LLoadString(IDS_ATTACH_WARNING_DLG), LLoadString(IDS_ATTACHMENTS), MB_YESNO); if (Result == IDNO) { Add = false; break; } } } if (Add) { Attachment *a = GetItem()->AttachFile(this, Path); if (a) { Attachments->Insert(a); Attachments->ResizeColumnsToContent(); } } } } ////////////////////////////////////////////////////////////////////////////// bool Mail::PreviewLines = false; bool Mail::RunMailPipes = true; List Mail::NewMailLst; bool Mail::AdjustDateTz = true; LHashTbl,Mail*> Mail::MessageIdMap; Mail::Mail(ScribeWnd *app, LDataI *object) : Thing(app, object) { DefaultObject(object); d = new MailPrivate(this); _New(); } Mail::~Mail() { if (GetObject()) { auto Id = GetObject()->GetStr(FIELD_MESSAGE_ID); if (Id) MessageIdMap.Delete(Id); } NewMailLst.Delete(this); _Delete(); DeleteObj(d); } void Mail::_New() { SendAttempts = 0; Container = 0; FlagsCache = -1; PreviewCacheX = 0; Cursor = 0; ParentFile = 0; PreviousMail = 0; NewEmail = NewEmailNone; Ui = 0; TotalSizeCache = -1; } void Mail::_Delete() { if (ParentFile) { ParentFile->SetMsg(0); } UnloadAttachments(); PreviewCache.DeleteObjects(); DeleteObj(Container); if (Ui) Ui->PostEvent(M_CLOSE); if (Ui) Ui->SetItem(0); } bool Mail::AppendItems(LSubMenu *Menu, const char *Param, int Base) { auto Remove = Menu->AppendSub(LLoadString(IDS_REMOVE)); if (Remove) { Remove->AppendItem("'>'", IDM_REMOVE_GRTH, true); Remove->AppendItem("'> '", IDM_REMOVE_GRTH_SP, true); Remove->AppendItem(LLoadString(IDS_HTML_TAGS), IDM_REMOVE_HTML, true); } auto FilterMenu = Menu->AppendSub(LLoadString(IDS_FILTER)); if (FilterMenu) { FilterMenu->SetImageList(App->GetIconImgList(), false); Actions.Empty(); AddActions(FilterMenu, Actions, App->GetThingSources(MAGIC_FILTER)); } auto Convert = Menu->AppendSub(LLoadString(IDS_CONVERT_SELECTION)); if (Convert) { Convert->AppendItem("To Base64", IDM_CONVERT_BIN_TO_B64, true); Convert->AppendItem("To Binary", IDM_CONVERT_B64_TO_BIN, true); } if (!TestFlag(GetFlags(), MAIL_CREATED)) { auto Charset = Menu->AppendSub(LLoadString(L_CHANGE_CHARSET)); if (Charset) { int n=0; for (LCharset *c = LGetCsList(); c->Charset; c++, n++) Charset->AppendItem(c->Charset, IDM_CHARSET_BASE + n, c->IsAvailable()); } } return true; } bool Mail::OnMenu(LDocView *View, int Id, void *Context) { const char *RemoveStr = 0; switch (Id) { case IDM_REMOVE_GRTH: { RemoveStr = ">"; break; } case IDM_REMOVE_GRTH_SP: { RemoveStr = "> "; break; } case IDM_REMOVE_HTML: { auto s = View ? View->Name() : 0; if (s) { auto n = DeHtml(s); if (n) { View->Name(n); DeleteArray(n); } } break; } default: { if (Id >= IDM_CHARSET_BASE) { int n=0; LCharset *c; for (c = LGetCsList(); c->Charset; c++, n++) { if (Id - IDM_CHARSET_BASE == n) { break; } } if (c->Charset) { SetBodyCharset((char*)c->Charset); SetDirty(); if (GetBodyCharset() && Ui) { Ui->OnLoad(); } } } else if (Id >= IDM_FILTER_BASE) { Filter *Action = Actions[Id-IDM_FILTER_BASE]; if (Action) { // Save the message... SetDirty(false); // Hide the mail window... if (Ui) Ui->Visible(false); // Do the action bool Stop; Mail *This = this; Action->DoActions(This, Stop); if (This != this) break; if (Ui) DeleteObj(Ui); return true; } } break; } #ifdef _DEBUG case IDM_CONVERT_BIN_TO_B64: { char *t = View->GetSelection(); if (t) { size_t In = strlen(t); size_t Len = BufferLen_BinTo64(In); char *B64 = new char[Len+1]; if (B64) { ConvertBinaryToBase64(B64, Len, (uchar*)t, In); B64[Len] = 0; char Temp[256]; ConvertBase64ToBinary((uchar*)Temp, sizeof(Temp), B64, Len); char16 *Str = Utf8ToWide(B64); if (Str) { LTextView3 *Tv = dynamic_cast(View); if (Tv) { Tv->DeleteSelection(); Tv->Insert(Tv->GetCaret(), Str, Len); } DeleteArray(Str); } DeleteArray(B64); } } break; } case IDM_CONVERT_B64_TO_BIN: { char *t = View->GetSelection(); if (t) { size_t In = strlen(t); size_t Len = BufferLen_64ToBin(In); char *Bin = new char[Len+1]; if (Bin) { ssize_t Out = ConvertBase64ToBinary((uchar*)Bin, Len, t, In); Bin[Out] = 0; char16 *Str = Utf8ToWide(Bin, Out); if (Str) { LTextView3 *Tv = dynamic_cast(View); if (Tv) { Tv->DeleteSelection(); Tv->Insert(Tv->GetCaret(), Str, Out); } DeleteArray(Str); } DeleteArray(Bin); } } break; } #endif } if (RemoveStr) { size_t TokenLen = strlen(RemoveStr); auto s = View ? View->Name() : 0; if (s) { LMemQueue Temp; auto Start = s; const char *End; while (*Start) { // seek to EOL for (End = Start; *End && *End != '\n'; End++); End++; ssize_t Len = End - Start; if (_strnicmp(Start, RemoveStr, TokenLen) == 0) { Temp.Write((uchar*)Start + TokenLen, Len - TokenLen); } else { Temp.Write((uchar*)Start, Len); } Start = End; } int Size = (int)Temp.GetSize(); char *Buf = new char[Size+1]; if (Buf) { Temp.Read((uchar*) Buf, Size); Buf[Size] = 0; View->Name(Buf); DeleteArray(Buf); } } } return true; } LDocumentEnv::LoadType Mail::GetContent(LoadJob *&j) { if (!j) return LoadError; LUri Uri(j->Uri); if ( Uri.sProtocol && ( !_stricmp(Uri.sProtocol, "http") || !_stricmp(Uri.sProtocol, "https") || !_stricmp(Uri.sProtocol, "ftp") ) ) { // We don't check OPT_HtmlLoadImages here because it's done elsewhere: // - ScribeWnd::CreateTextControl calls LHtml::SetLoadImages with the value from OPT_HtmlLoadImages // - LTag::LoadImage checks LHtml::GetLoadImages // // If there is a remote job here, it's because it's probably whitelisted. if (!Worker) Worker = App->GetImageLoader(); if (!Worker) return LoadError; Worker->AddJob(j); j = 0; return LoadDeferred; } else if (Uri.sProtocol && !_stricmp(Uri.sProtocol, "file")) { if (!_strnicmp(Uri.sPath, "//", 2)) { // This seems to hang the windows CreateFile function... } else { // Is it a local file then? if (j->pDC.Reset(GdcD->Load(Uri.sPath))) { return LoadImmediate; } } } else { List Files; if (GetAttachments(&Files)) { auto Dir = FilePart(j->Uri); Attachment *a = NULL; for (auto It = Files.begin(); It != Files.end(); It++) { a = *It; if (_strnicmp(j->Uri, "cid:", 4) == 0) { char *ContentId = j->Uri + 4; auto AttachmentId = a->GetContentId(); if (AttachmentId) { if (AttachmentId[0] == '<') { auto s = AttachmentId + 1; auto e = strrchr(s, '>'); if (e) { ssize_t len = e - s; if (strlen(ContentId) == len && !strncmp(s, ContentId, len)) break; } } else if (!strcmp(AttachmentId, ContentId)) { break; } } } else { auto Name = a->GetName(); if (Name) { auto NameDir = FilePart(Name); if (_stricmp(NameDir, Dir) == 0) { break; } } } } if (a) { j->MimeType = a->GetMimeType(); j->ContentId = a->GetContentId(); if (j->Pref == LoadJob::FmtStream) { j->Filename = a->GetName(); j->Stream = a->GetObject()->GetStream(_FL); return LoadImmediate; } else { char *Tmp = ScribeTempPath(); if (Tmp) { auto File = a->GetName(); auto Ext = LGetExtension(File); char s[MAX_PATH_LEN] = ""; LString part; do { if (part.Printf("%x.%s", LRand(), Ext) < 0) return LoadError; if (!LMakePath(s, sizeof(s), Tmp, part)) return LoadError; } while (LFileExists(s)); if (a->SaveTo(s, true)) { if (j->Pref == LoadJob::FmtFilename) { j->Filename = s; return LoadImmediate; } else { int Promote = GdcD->SetOption(GDC_PROMOTE_ON_LOAD, 0); j->pDC.Reset(GdcD->Load(s)); j->Filename = a->GetName(); GdcD->SetOption(GDC_PROMOTE_ON_LOAD, Promote); FileDev->Delete(s, false); return LoadImmediate; } } } } } } } return LoadError; } class MailCapabilities : public LLayout { LArray MissingCaps; LDocView *Doc; LButton *Install; public: MailCapabilities(LDocView *d) { Doc = d; Install = 0; } const char *GetClass() { return "MailCapabilities"; } void OnPosChange() { LRect c = GetClient(); if (MissingCaps.Length()) { if (!Install) { if ((Install = new LButton(IDOK, 0, 0, -1, -1, "Install"))) Install->Attach(this); } LRect r = c; r.x1 = r.x2 - Install->X(); r.y2 -= 7; Install->SetPos(r); } } void OnPaint(LSurface *pDC) { LRect cli = GetClient(); if (MissingCaps.Length()) { char Msg[256]; int c = sprintf_s(Msg, sizeof(Msg), "This content requires "); for (unsigned i=0; iTransparent(false); LSysFont->Colour(L_TEXT, L_MED); ds.Draw(pDC, cli.x1, cli.y1, &cli); } else { pDC->Colour(L_MED); pDC->Rectangle(); } } }; LDocView *Mail::CreateView( MailViewOwner *Owner, LString MimeType, bool Sunken, size_t MaxBytes, bool NoEdit) { bool Created = TestFlag(GetFlags(), MAIL_CREATED); bool Edit = NoEdit ? false : Created; bool ReadOnly = !Created; LAutoString Mem; LVariant DefAlt; App->GetOptions()->GetValue(OPT_DefaultAlternative, DefAlt); auto TextBody = GetBody(); auto TextCharset = GetBodyCharset(); auto HtmlBody = GetHtml(); auto HtmlCharset = GetHtmlCharset(); const char *CtrlType = NULL; if (!MimeType) { bool TextValid = TextBody != NULL; bool HtmlValid = HtmlBody != NULL; if (TextValid && HtmlValid) MimeType = DefAlt.CastInt32() ? sTextHtml : sTextPlain; else if (TextValid) MimeType = sTextPlain; else if (HtmlValid) MimeType = sTextHtml; else return NULL; } #ifdef WINDOWS if (DefAlt.CastInt32() == 2 && MimeType == sTextHtml) CtrlType = sApplicationInternetExplorer; else #endif CtrlType = MimeType; const char *Content, *Charset; if (MimeType == sTextHtml) { Content = HtmlBody; Charset = HtmlCharset; } else { Content = TextBody; Charset = TextCharset; } // Emoji check LVariant NoEmoji; App->GetOptions()->GetValue(OPT_NoEmoji, NoEmoji); // Check if the control needs changing LDocView *View = Owner->GetDoc(MimeType); if (View) { const char *ViewMimeType = View->GetMimeType(); if (MimeType != ViewMimeType) { Owner->SetDoc(NULL, ViewMimeType); View = NULL; } } if (!View) { View = App->CreateTextControl( MimeType == sTextHtml ? IDC_HTML_VIEW : IDC_TEXT_VIEW, MimeType, Edit, this); } if (View) { // Control setup View->Sunken(Sunken); View->SetReadOnly(ReadOnly); View->SetEnv(this); LVariant UseCid = true; View->SetValue(LDomPropToString(HtmlImagesLinkCid), UseCid); LVariant LoadImages; App->GetOptions()->GetValue(OPT_HtmlLoadImages, LoadImages); bool AppLoadImages = LoadImages.CastInt32() != 0; bool MailLoadImages = TestFlag(GetFlags(), MAIL_SHOW_IMAGES); const char *SenderAddr = GetFrom() ? GetFrom()->GetStr(FIELD_EMAIL) : NULL; auto SenderStatus = App->RemoteContent_GetSenderStatus(SenderAddr); View->SetLoadImages ( SenderStatus != RemoteNeverLoad && ( AppLoadImages || MailLoadImages || SenderStatus == RemoteAlwaysLoad ) ); // Attach control Owner->SetDoc(View, MimeType); LCharset *CsInfo = LGetCsInfo(Charset); // Check for render scripts LArray Renderers; LString RenderMsg = "Rendering..."; if (App->GetScriptCallbacks(LRenderMail, Renderers)) { for (auto r: Renderers) { LVirtualMachine Vm; LScriptArguments Args(&Vm); Args.New() = new LVariant(App); Args.New() = new LVariant(this); Args.New() = new LVariant((void*)NULL); bool Status = App->ExecuteScriptCallback(*r, Args); Args.DeleteObjects(); if (Status) { auto Ret = Args.GetReturn(); if (Ret->IsString() || Ret->CastInt32()) { if (Ret->IsString()) RenderMsg = Ret->Str(); d->Renderer.Reset(new MailRendererScript(this, r)); break; } } } } if (d->Renderer) { LString Nm; if (MimeType.Equals(sTextHtml)) Nm.Printf("%s", RenderMsg.Get()); else Nm = RenderMsg; View->Name(Nm); } else { // Send the data to the control size_t ContentLen = Content ? strlen(Content) : 0; Html1::LHtml *Html = dynamic_cast(View); if (MimeType.Equals(sTextHtml)) { if (CsInfo) { int OverideDocCharset = *Charset == '>' ? 1 : 0; View->SetCharset(Charset + OverideDocCharset); if (Html) Html->SetOverideDocCharset(OverideDocCharset != 0); } else { View->SetCharset(0); if (Html) Html->SetOverideDocCharset(0); } View->Name(Content); } else { LAutoPtr Utf32((uint32_t*)LNewConvertCp("utf-32", Content, Charset ? Charset : (char*)"utf-8", MaxBytes > 0 ? MIN(ContentLen, MaxBytes) : ContentLen)); if (Utf32) { int Len = 0; while (Utf32[Len]) Len++; #if 0 LFile f; if (f.Open("c:\\temp\\utf32.txt", O_WRITE)) { uchar bom[4] = { 0xff, 0xfe, 0, 0 }; f.Write(bom, 4); f.Write(Utf32, Len * sizeof(uint32)); f.Close(); } #endif } LAutoWString Wide; Wide.Reset((char16*)LNewConvertCp(LGI_WideCharset, Content, Charset ? Charset : (char*)"utf-8", MaxBytes > 0 ? MIN(ContentLen, MaxBytes) : ContentLen)); if (Wide) { View->NameW(Wide); } else { // Fallback... try and show something at least LAutoString t(NewStr(Content, MaxBytes > 0 ? MIN(ContentLen, MaxBytes) : ContentLen)); if (t) { uint8_t *i = (uint8_t*)t.Get(); while (*i) { if (*i & 0x80) *i &= 0x7f; i++; } View->Name(t); } else View->NameW(0); } View->SetFixedWidthFont(TestFlag(GetFlags(), MAIL_FIXED_WIDTH_FONT)); } } } return View; } bool Mail::OnNavigate(LDocView *Parent, const char *Uri) { if (Uri) { if ( _strnicmp(Uri, "mailto:", 7) == 0 || ( strchr(Uri, '@') && !strchr(Uri, '/') ) ) { // Mail address return App->CreateMail(0, Uri, 0) != 0; } else { return LDefaultDocumentEnv::OnNavigate(Parent, Uri); } } return false; } void Mail::Update() { TotalSizeCache = -1; LListItem::Update(); } void ParseIdList(char *In, List &Out) { if (!In) return; while (*In && strchr(WhiteSpace, *In)) In++; if (*In == '<') { // Standard msg-id list.. for (char *s=In; s && *s; ) { s = strchr(s, '<'); if (!s) break; while (*s == '<') s++; char *e = strchr(s, '>'); if (e) { Out.Insert(NewStr(s, e-s)); s = e + 1; } else break; } } else { // Non compliant msg-id list... const char Delim[] = ", \t\r\n"; for (char *s=In; s && *s; ) { if (strchr(Delim, *s)) s++; else { char *Start = s; while (*s && !strchr(Delim, *s)) s++; Out.Insert(NewStr(Start, s - Start)); } } } } void Base36(char *Out, uint64 In) { while (In) { int p = (int)(In % 36); if (p < 10) { *Out++ = '0' + p; } else { *Out++ = 'A' + p - 10; } In /= 36; } *Out++ = 0; } void Mail::ClearCachedItems() { TotalSizeCache = -1; PreviewCacheX = -1; PreviewCache.DeleteObjects(); Attachment *a; int i = 0; while ((a = Attachments[i])) { if (!a->DecRef()) i++; } } void Mail::NewRecipient(char *Email, char *Name) { LDataPropI *a = GetTo()->Create(GetObject()->GetStore()); if (a) { a->SetStr(FIELD_EMAIL, Email); a->SetStr(FIELD_NAME, Name); GetTo()->Insert(a); } } void Mail::PrepSend() { // Set flags and other data SetDirty(); int OldFlags = GetFlags(); SetFlags((OldFlags | MAIL_READY_TO_SEND) & ~MAIL_SENT); // we want to send now... // Check we're in the Outbox ScribeFolder *OutBox = App->GetFolder(FOLDER_OUTBOX, GetObject()); if (OutBox) { ScribeFolder *f = GetFolder(); if (!f || f != OutBox) { LArray Items; Items.Add(this); OutBox->MoveTo(Items); } } } bool Mail::Send(bool Now) { // Check for any "on before send" callbacks: bool AllowSend = true; LArray Callbacks; if (App->GetScriptCallbacks(LMailOnBeforeSend, Callbacks)) { for (unsigned i=0; AllowSend && iExecuteScriptCallback(c, Args)) { if (!Args.GetReturn()->CastInt32()) AllowSend = false; } Args.DeleteObjects(); } } } if (!AllowSend) return false; // Set the ready to send flag.. bool IsInPublicFolder = GetFolder() && GetFolder()->IsPublicFolders(); if (!IsInPublicFolder) { PrepSend(); if (Now) { // Kick off send thread if relevant LVariant Offline; App->GetOptions()->GetValue(OPT_WorkOffline, Offline); if (!Offline.CastInt32() && !IsInPublicFolder) { App->PostEvent(M_COMMAND, IDM_SEND_MAIL, (LMessage::Param)Handle()); } } } return true; } bool Mail::MailMessageIdMap(bool Add) { if (Add) { auto LoadState = GetLoaded(); if (LoadState < Store3Headers) { LAssert(!"Not loaded yet."); LStackTrace("MailMessageIdMap msg not loaded yet: %i\n", LoadState); return false; } // char *ObjId = GetObject()->GetStr(FIELD_MESSAGE_ID); auto Id = GetMessageId(true); if (!Id) { LAssert(!"No message ID? Impossible!"); GetMessageId(true); return false; } #if 0 LgiTrace("MailMessageIdMap(%i) Old=%s Id=%s Mail=%p\n", Add, ObjId, Id, this); #endif return MessageIdMap.Add(Id, this); } else { auto Id = GetMessageId(); #if 0 LgiTrace("MailMessageIdMap(%i) Id=%s Mail=%p\n", Add, Id, this); #endif return MessageIdMap.Delete(Id); } } Mail *Mail::GetMailFromId(const char *Id) { Mail *m = MessageIdMap.Find(Id); // LgiTrace("GetMailFromId(%s)=%p\n", Id, m); return m; } bool Mail::SetMessageId(const char *MsgId) { if (LAppInst->InThread()) { LAssert(GetObject() != NULL); auto OldId = GetObject()->GetStr(FIELD_MESSAGE_ID); if (OldId) MessageIdMap.Delete(OldId); LAutoString m(NewStr(MsgId)); LAutoString s(TrimStr(m, "<>")); if (GetObject()->SetStr(FIELD_MESSAGE_ID, s)) SetDirty(); return s != 0; } else { // No no no NO NON NOT NADA. LAssert(0); } return false; } LAutoString Mail::GetThreadIndex(int TruncateChars) { LAutoString Id; LAutoString Raw(InetGetHeaderField(GetInternetHeader(), "Thread-Index")); if (Raw) { Id = ConvertThreadIndex(Raw, TruncateChars); } return Id; } const char *Mail::GetMessageId(bool Create) { LAssert(GetObject() != NULL); d->MsgIdCache = GetObject()->GetStr(FIELD_MESSAGE_ID); if (!d->MsgIdCache) { bool InThread = GetCurrentThreadId() == LAppInst->GetGuiThreadId(); LAutoString Header(InetGetHeaderField(GetInternetHeader(), "Message-ID")); if (Header) { LAssert(InThread); if (InThread) { List Ids; ParseIdList(Header, Ids); SetMessageId(Ids[0]); Ids.DeleteArrays(); d->MsgIdCache = GetObject()->GetStr(FIELD_MESSAGE_ID); } } if (!d->MsgIdCache && Create) { LAssert(InThread); if (InThread) { auto FromEmail = GetFromStr(FIELD_EMAIL); const char *At = FromEmail ? strchr(FromEmail, '@') : 0; if (!At) { LVariant Email; if (App->GetOptions()->GetValue(OPT_Email, Email) && Email.Str()) { At = strchr(Email.Str(), '@'); } else { At = "@domain.com"; } } if (At) { char m[96], a[32], b[32]; Base36(a, LCurrentTime()); Base36(b, LRand(RAND_MAX)); sprintf_s(m, sizeof(m), "<%s.%i%s%s>", a, LRand(RAND_MAX), b, At); if (GetObject()->SetStr(FIELD_MESSAGE_ID, m)) SetDirty(); d->MsgIdCache = GetObject()->GetStr(FIELD_MESSAGE_ID); } else LgiTrace("%s:%i - Error, no '@' in %s.\n", _FL, FromEmail); } else LgiTrace("%s:%i - Error, not in thread.\n", _FL); } } else { d->MsgIdCache = d->MsgIdCache.Strip("<>").Strip(); } LAssert ( (!d->MsgIdCache && !Create) || (d->MsgIdCache && !strchr(d->MsgIdCache, '\n')) ); return d->MsgIdCache; } bool Mail::GetReferences(List &Ids) { LAutoString References(InetGetHeaderField(GetInternetHeader(), "References")); if (References) { ParseIdList(References, Ids); } LAutoString InReplyTo(InetGetHeaderField(GetInternetHeader(), "In-Reply-To")); if (InReplyTo) { List To; ParseIdList(InReplyTo, To); bool Has = false; char *r = To[0]; if (r) { for (auto h: Ids) { if (!strcmp(h, r)) Has = true; } if (!Has) { To.Delete(r); Ids.Insert(r); } } To.DeleteArrays(); } if (Ids.Length() == 0) { LAutoString Id = GetThreadIndex(5); if (Id) { size_t Len = strlen(Id); size_t Bytes = Len >> 1; if (Bytes >= 22) { Ids.Insert(Id.Release()); } } } return Ids.Length() > 0; } LString AddrToHtml(LDataPropI *a) { LString s; if (a) { auto Name = a->GetStr(FIELD_NAME); auto Addr = a->GetStr(FIELD_EMAIL); LXmlTree t; LAutoString eName(t.EncodeEntities(Name)); LAutoString eAddr(t.EncodeEntities(Addr)); if (Name && Addr) s.Printf("%s", eAddr.Get(), eName.Get()); else if (Name) s = eName.Get(); else if (Addr) s.Printf("%s", eAddr.Get(), eAddr.Get()); } return s; } LDataPropI *FindMimeSeg(LDataPropI *s, char *Type) { if (!s) return 0; const char *Mt = s->GetStr(FIELD_MIME_TYPE); if (!Mt) Mt = "text/plain"; if (!_stricmp(Type, Mt)) return s; - GDataIt c = s->GetList(FIELD_MIME_SEG); + LDataIt c = s->GetList(FIELD_MIME_SEG); if (!c) return 0; for (LDataPropI *i=c->First(); i; i=c->Next()) { LDataPropI *Child = FindMimeSeg(i, Type); if (Child) return Child; } return 0; } bool Mail::GetAttachmentObjs(LArray &Objs) { if (!GetObject()) return false; CollectAttachments(&Objs, NULL, NULL, NULL, GetObject()->GetObj(FIELD_MIME_SEG)); return Objs.Length() > 0; } void DescribeMime(LStream &p, LDataI *Seg) { auto Mt = Seg->GetStr(FIELD_MIME_TYPE); p.Print("%s", Mt); auto Cs = Seg->GetStr(FIELD_CHARSET); if (Cs) p.Print(" - %s", Cs); p.Print("
\n"); - GDataIt c = Seg->GetList(FIELD_MIME_SEG); + LDataIt c = Seg->GetList(FIELD_MIME_SEG); if (c && c->First()) { p.Print("
\n"); for (LDataPropI *i=c->First(); i; i=c->Next()) { LDataI *a = dynamic_cast(i); if (a) DescribeMime(p, a); } p.Print("
\n"); } } bool Mail::GetVariant(const char *Name, LVariant &Value, const char *Array) { ScribeDomType Fld = StrToDom(Name); switch (Fld) { case SdFrom: // Type: ListAddr { Value = GetFrom(); break; } case SdFromHtml: // Type: String { LString s = AddrToHtml(GetFrom()); if (s.Length() == 0) return false; Value = s; break; } case SdContact: // Type: Contact { if (!GetFrom()) return false; auto Addr = GetFrom()->GetStr(FIELD_EMAIL); if (!Addr) return false; Contact *c = Contact::LookupEmail(Addr); if (!c) return App->GetVariant("NoContact", Value); Value = (LDom*)c; break; } case SdTo: // Type: ListAddr[] { if (Array) { - GDataIt To = GetTo(); + LDataIt To = GetTo(); int Idx = atoi(Array); bool Create = false; if (Idx < 0) { Create = true; Idx = -Idx; } LDataPropI *a = Idx < (int)To->Length() ? (*To)[Idx] : 0; if (!a && Create) { if ((a = To->Create(GetObject()->GetStore()))) { To->Insert(a); } } Value = a; } else if (Value.SetList()) { - GDataIt To = GetTo(); + LDataIt To = GetTo(); for (LDataPropI *a=To->First(); a; a=To->Next()) { LVariant *Recip = new LVariant; if (Recip) { *Recip = a; Value.Value.Lst->Insert(Recip); } } } else return false; break; } case SdToHtml: // Type: String { if (GetTo()) { LStringPipe p; for (LDataPropI *t=GetTo()->First(); t; t=GetTo()->Next()) { if (p.GetSize()) p.Write((char*)", ", 2); LString s = AddrToHtml(t); if (s) p.Write(s, s.Length()); } Value.Type = GV_STRING; Value.Value.String = p.NewStr(); } break; } case SdSubject: // Type: String { Value = GetSubject(); break; } case SdBody: // Type: String { if (Array) { auto Body = GetBody(); LAutoString h; for (auto c = Body; c && *c; ) { while (*c && strchr(WhiteSpace, *c)) c++; auto Start = c; while (*c && *c != ':' && *c != '\n') c++; if (*c == ':') { if (Start[0] == '\"' && c[1] == '\"') c++; LString s(Start, c - Start); s = s.Strip("\'\" \t[]<>{}:"); if (s.Equals(Array)) { // Match... c++; while (*c && strchr(WhiteSpace, *c)) c++; Start = c; while (*c && *c != '\n') c++; while (c > Start && strchr(WhiteSpace, c[-1])) c--; h.Reset(NewStr(Start, c - Start)); break; } else { while (*c && *c != '\n') c++; } } if (*c == '\n') c++; } if (h) { if (GetBodyCharset()) { char *u = (char*)LNewConvertCp("utf-8", h, GetBodyCharset()); if (u) { Value.OwnStr(u); } else { Value.OwnStr(h.Release()); } } else { Value.OwnStr(h.Release()); } } } else { Value = GetBody(); } break; } case SdBodyAsText: // Type: String { size_t MaxSize = ValidStr(Array) ? atoi(Array) : -1; if (ValidStr(GetBody()) && ValidStr(GetHtml())) { LVariant Def; App->GetOptions()->GetValue(OPT_DefaultAlternative, Def); if (Def.CastInt32()) { goto DoHtml; } goto DoText; } else if (ValidStr(GetBody())) { DoText: LAutoString CharSet = GetCharSet(); auto Txt = GetBody(); if (CharSet) { size_t TxtLen = strlen(Txt); Value.OwnStr((char*)LNewConvertCp("utf-8", Txt, CharSet, MIN(TxtLen, MaxSize))); } else Value = Txt; } else if (ValidStr(GetHtml())) { DoHtml: auto v = HtmlToText(GetHtml(), GetHtmlCharset()); Value = v.Get(); } else return false; break; } case SdBodyAsHtml: // Type: String { // int MaxSize = ValidStr(Array) ? atoi(Array) : -1; MailPrivate::HtmlBody *b = d->GetBody(); if (!b) return false; LStringPipe p; p.Print("
\n%s\n
\n", ScribeReplyClass, b->Html.Get()); Value.OwnStr(p.NewStr()); LgiTrace("Value=%s\n", Value.Str()); break; } case SdHtmlHeadFields: // Type: String { MailPrivate::HtmlBody *b = d->GetBody(); if (!b) return false; LStringPipe p; if (ValidStr(b->Charset)) p.Print("\t\n", b->Charset.Get()); p.Print("\t"); Value.OwnStr(p.NewStr()); break; } case SdMessageID: // Type: String { Value = GetMessageId(); return true; } case SdInternetHeaders: // Type: String { Value = GetInternetHeader(); break; } case SdInternetHeader: // Type: String[] { if (!Array || !GetInternetHeader()) return false; LAutoString s(InetGetHeaderField(GetInternetHeader(), Array)); if (s) Value = s; else Value.Empty(); break; } case SdPriority: // Type: Int32 { Value = (int)GetPriority(); break; } case SdHtml: // Type: String { Value = GetHtml(); break; } case SdFlags: // Type: Int32 { if (Array) { int Flag = StringToMailFlag(Array); Value = (GetFlags() & Flag) != 0; } else { Value = (int)GetFlags(); } break; } case SdFolder: // Type: ScribeFolder { ScribeFolder *f = GetFolder(); if (!f) return false; Value = (LDom*)f; break; } case SdScribe: // Type: ScribeWnd { Value = (LDom*)App; break; } case SdMail: // Type: Mail { Value = PreviousMail; break; } case SdDateSent: // Type: DateTime { Value = GetDateSent(); break; } case SdDateReceived: // Type: DateTime { Value = GetDateReceived(); break; } case SdSig: // Type: String { bool Type = false; if (Array && stristr(Array, "html")) Type = true; Value = GetSig(Type); break; } case SdSize: // Type: Int64 { Value = TotalSizeof(); break; } case SdLabel: // Type: String { Value = GetLabel(); break; } case SdAttachments: // Type: Int32 { LArray Lst; GetAttachmentObjs(Lst); Value = (int)Lst.Length(); break; } case SdAttachment: // Type: Attachment[] { List Files; if (GetAttachments(&Files) && Array) { int i = atoi(Array); Value = Files[i]; if (Value.Type == GV_DOM) return true; } LAssert(!"Not a valid attachment?"); return false; } case SdMimeTree: // Type: String { LDataI *Root = dynamic_cast(GetObject()->GetObj(FIELD_MIME_SEG)); if (!Root) return false; LStringPipe p(256); DescribeMime(p, Root); Value.OwnStr(p.NewStr()); break; } case SdUi: // Type: LView { Value = Ui; break; } case SdType: // Type: Int32 { Value = GetObject()->Type(); break; } case SdSelected: // Type: Bool { Value = Select(); break; } case SdRead: // Type: Bool { Value = (GetFlags() & MAIL_READ) != 0; break; } case SdShowImages: // Type: Bool { Value = (GetFlags() & MAIL_SHOW_IMAGES) != 0; break; } case SdColour: // Type: Int32 { Value = GetMarkColour(); break; } case SdReceivedDomain: // Type: String { Value = GetFieldText(FIELD_RECEIVED_DOMAIN); break; } default: { return false; } } return true; } #define DomSetStr(Dom, Fld) \ case Dom: \ if (GetObject() && Value.Type == GV_STRING) \ GetObject()->SetStr(Fld, Value.Str()); \ else \ LAssert(!"Missing object or type err"); \ break; #define DomSetInt(Dom, Fld) \ case Dom: \ if (GetObject() && Value.Type == GV_INT32) \ GetObject()->SetInt(Fld, Value.Value.Int); \ else \ LAssert(!"Missing object or type err"); \ break; #define DomSetDate(Dom, Fld) \ case Dom: \ if (GetObject() && Value.Type == GV_DATETIME) \ GetObject()->SetDate(Fld, Value.Value.Date); \ else \ LAssert(!"Missing object or type err"); \ break; bool Mail::SetVariant(const char *Name, LVariant &Value, const char *Array) { ScribeDomType Fld = StrToDom(Name); switch (Fld) { DomSetStr(SdSubject, FIELD_SUBJECT) DomSetStr(SdMessageID, FIELD_MESSAGE_ID) DomSetStr(SdInternetHeaders, FIELD_INTERNET_HEADER) DomSetInt(SdPriority, FIELD_PRIORITY) DomSetDate(SdDateSent, FIELD_DATE_SENT) DomSetDate(SdDateReceived, FIELD_DATE_RECEIVED) case SdBody: { if (!SetBody(Value.Str())) return false; break; } case SdHtml: { if (!SetHtml(Value.Str())) return false; break; } case SdRead: { if (Value.CastInt32()) SetFlags(GetFlags() | MAIL_READ); else SetFlags(GetFlags() & ~MAIL_READ); break; } case SdColour: { auto u32 = Value.IsNull() ? 0 : (uint32_t)Value.CastInt32(); if (!SetMarkColour(u32)) return false; break; } case SdShowImages: { if (Value.CastInt32()) SetFlags(GetFlags() | MAIL_SHOW_IMAGES); else SetFlags(GetFlags() & ~MAIL_SHOW_IMAGES); break; } case SdSelected: { Select(Value.CastInt32() != 0); break; } case SdLabel: { if (!SetLabel(Value.Str())) return false; break; } case SdFlags: { if (Array) { int Flag = StringToMailFlag(Array); if (Value.CastInt32()) // Set SetFlags(GetFlags() | Flag); else SetFlags(GetFlags() & ~Flag); } else { SetFlags(Value.CastInt32()); } break; } default: { return false; } } SetDirty(); Update(); return true; } bool Mail::CallMethod(const char *MethodName, LVariant *ReturnValue, LArray &Args) { ScribeDomType Fld = StrToDom(MethodName); switch (Fld) { default: break; case SdSend: // Type: ([Bool SendNow = true]) { bool Now = Args.Length() > 0 ? Args[0]->CastInt32() != 0 : true; bool Result = Send(Now); if (ReturnValue) *ReturnValue = Result; return true; } case SdAddCalendarEvent: // Type: ([Bool AddPopupReminder]) { bool AddPopupReminder = Args.Length() > 0 ? Args[0]->CastInt32() != 0 : true; bool Result = AddCalendarEvent(NULL, AddPopupReminder, NULL); if (ReturnValue) *ReturnValue = Result; return true; } case SdGetRead: // Type: () { *ReturnValue = (GetFlags() & MAIL_READ) != 0; return true; } case SdSetRead: // Type: (Bool IsRead = true) { bool Rd = Args.Length() ? Args[0]->CastInt32() != 0 : true; auto Flags = GetFlags(); if (Rd) SetFlags(Flags | MAIL_READ); else SetFlags(Flags & ~MAIL_READ); *ReturnValue = true; return true; } case SdBayesianChange: // Type: (int SpamWordOffset, int HamWordOffset) { if (Args.Length() != 2) { LgiTrace("%s:%i - Invalid arg count, expecting (int SpamWordOffset, int HamWordOffset)\n", _FL); *ReturnValue = false; return true; } auto SpamWordOffset = Args[0]->CastInt32(); auto HamWordOffset = Args[1]->CastInt32(); if (SpamWordOffset > 1) App->OnBayesianMailEvent(this, BayesMailUnknown, BayesMailSpam); else if (SpamWordOffset < 0) App->OnBayesianMailEvent(this, BayesMailSpam, BayesMailUnknown); if (HamWordOffset > 1) App->OnBayesianMailEvent(this, BayesMailUnknown, BayesMailHam); else if (HamWordOffset < 1) App->OnBayesianMailEvent(this, BayesMailHam, BayesMailUnknown); break; } case SdBayesianScore: // Type: () { double Result; if (App->IsSpam(Result, this)) { *ReturnValue = Result; return true; } break; } case SdSearchHtml: // Type: (String SearchExpression) { if (Args.Length() != 2) { LgiTrace("%s:%i - Method needs 1 argument.\n", _FL); *ReturnValue = false; return true; } auto Html = GetHtml(); if (!Html) { LgiTrace("%s:%i - No HTML to parse.\n", _FL); *ReturnValue = false; return true; } // auto Cs = GetHtmlCharset(); SearchHtml(ReturnValue, Html, Args[0]->Str(), Args[1]->Str()); return true; } case SdDeleteAsSpam: // Type: () { DeleteAsSpam(App); return true; } } return Thing::CallMethod(MethodName, ReturnValue, Args); } char *Mail::GetDropFileName() { if (!DropFileName) { LString Subj = GetSubject(); Subj = Subj.Strip(" \t\r\n."); DropFileName.Reset(MakeFileName(ValidStr(Subj) ? Subj.Get() : (char*)"Untitled", "eml")); } return DropFileName; } bool Mail::GetDropFiles(LString::Array &Files) { if (!GetDropFileName()) return false; if (!LFileExists(DropFileName)) { LAutoPtr F(new LFile); if (F->Open(DropFileName, O_WRITE)) { F->SetSize(0); if (!Export(AutoCast(F), sMimeMessage)) return false; } else return false; } if (!LFileExists(DropFileName)) return false; Files.Add(DropFileName.Get()); return true; } OsView Mail::Handle() { return #if LGI_VIEW_HANDLE (Ui) ? Ui->Handle() : #endif NULL; } Thing &Mail::operator =(Thing &t) { Mail *m = t.IsMail(); if (m) { #define CopyStr(dst, src) \ { DeleteArray(dst); dst = NewStr(src); } /* for (LDataPropI *a = m->GetTo()->First(); a; a = m->GetTo()->Next()) { LDataPropI *NewA = GetTo()->Create(Object->GetStore()); if (NewA) { *NewA = *a; GetTo()->Insert(NewA); } } */ if (GetObject() && m->GetObject()) { GetObject()->CopyProps(*m->GetObject()); } else LAssert(!"This shouldn't happen right?"); Update(); } return *this; } bool Mail::HasAlternateHtml(Attachment **Attach) { // New system of storing alternate HTML in a Mail object field. if (GetHtml()) return true; // Old way of storing alternate HTML, in an attachment. // pre v1.53 List Files; if (GetAttachments(&Files)) { for (auto a: Files) { if ((_stricmp(a->GetText(0), "attachment.html") == 0 || _stricmp(a->GetText(0), "index.html") == 0) && (a->GetMimeType() ? _stricmp(a->GetMimeType(), "text/html") == 0 : 1)) { if (Attach) { *Attach = a; } return true; } } } // None found return false; } char *Mail::GetAlternateHtml(List *Refs) { char *Status = 0; Attachment *Attach = 0; if (HasAlternateHtml(&Attach)) { if (GetHtml()) { // New system of storing alternate HTML in a Mail object field. Status = NewStr(GetHtml()); } else if (Attach) { // Old way of storing alternate HTML, in an attachment. // pre v1.53 char *Ptr; ssize_t Size; if (Attach->Get(&Ptr, &Size)) { Status = NewStr((char*)Ptr, Size); } } if (Status && Refs) { // Turn all the cid: references into file names... LStringPipe Out; char *n; for (char *s=Status; *s; s=n) { n = stristr(s, "\"cid:"); if (n) { // Extract cid n++; Out.Push(s, n-s); s = n += 4; while (*n && !strchr("\"\' >", *n)) n++; char *Cid = NewStr(s, n-s); if (Cid) { // Find attachment List Files; if (GetAttachments(&Files)) { for (auto a: Files) { if (a->GetContentId() && strcmp(a->GetContentId(), Cid) == 0) { Refs->Insert(a); Out.Push(a->GetName()); } } } } } else { Out.Push(s); break; } } DeleteArray(Status); Status = Out.NewStr(); } } return Status; } bool Mail::WriteAlternateHtml(char *DstFile, int DstFileLen) { bool Status = false; char *Tmp = ScribeTempPath(); List Refs; char *Html; if (Tmp && (Html = GetAlternateHtml(&Refs))) { char FileName[256]; LMakePath(FileName, sizeof(FileName), Tmp, "Alt.html"); if (DstFile) { strcpy_s(DstFile, DstFileLen, FileName); } LFile f; if (f.Open(FileName, O_WRITE)) { size_t Len = strlen(Html); Status = f.Write(Html, Len) == Len; f.Close(); } DeleteArray(Html); for (auto a: Refs) { LMakePath(FileName, sizeof(FileName), Tmp, a->GetName()); FileDev->Delete(FileName, false); a->SaveTo(FileName); } } return Status; } bool Mail::DeleteAttachment(Attachment *File) { bool Status = false; if (File) { if (Attachments.HasItem(File)) { Attachments.Delete(File); if (File->GetObject()) { auto r = File->GetObject()->Delete(); if (r > Store3Error) { auto o = File->GetObject(); if (o->IsOrphan()) o = NULL; // SetObject will delete... File->SetObject(NULL, false, _FL); DeleteObj(o); } } File->DecRef(); File = NULL; if (Attachments.Length() == 0) { // Remove attachments flag SetFlags(GetFlags() & (~MAIL_ATTACHMENTS)); } Update(); if (Ui) { Ui->OnAttachmentsChange(); } } } return Status; } bool Mail::UnloadAttachments() { for (auto it = Attachments.begin(); it != Attachments.end(); ) { Attachment *a = *it; if (!a->DecRef()) it++; } return true; } +LArray Mail::GetAttachments() +{ + LArray result; + + LArray Lst; + if (GetAttachmentObjs(Lst)) + { + LHashTbl, bool> Loaded; + for (size_t i=0; iGetObject(), true); + } + + // Load attachments + for (unsigned i=0; iSetOwner(this); + Attachments.Insert(k); + } + } + } + } + + for (auto a: Attachments) + { + if (a->GetObject()->UserData != a) + LAssert(!"Wut?"); + else + result.Add(a); + } + + return result; +} + bool Mail::GetAttachments(List *Files) { - bool Status = false; - if (Files) - { - LArray Lst; - if (GetAttachmentObjs(Lst)) - { - LHashTbl, bool> Loaded; - for (size_t i=0; iGetObject(), true); - } - - // Load attachments - for (unsigned i=0; iSetOwner(this); - Attachments.Insert(k); - } - } - } - } - - for (auto a: Attachments) - { - if (a->GetObject()->UserData != a) - { - LAssert(!"Wut?"); - } - Files->Insert(a); - } - Status = true; - } - else - { - Status = Attachments.Length() > 0; - } - return Status; + if (!Files) + return false; + auto files = GetAttachments(); + for (auto a: files) + Files->Add(a); + return true; } int64 Mail::TotalSizeof() { if (TotalSizeCache < 0) { int Size = GetObject() ? (int)GetObject()->GetInt(FIELD_SIZE) : -1; if (Size >= 0) { TotalSizeCache = Size; } else { TotalSizeCache = ((GetBody()) ? strlen(GetBody()) : 0) + ((GetHtml()) ? strlen(GetHtml()) : 0); List Attachments; if (GetAttachments(&Attachments)) { for (auto a: Attachments) { TotalSizeCache += a->GetSize(); } } } } return TotalSizeCache; } bool Mail::_GetListItems(List &l, bool All) { LList *ParentList = LListItem::Parent; l.Empty(); if (All) { if (ParentList) { ParentList->GetAll(l); } else { l.Insert(this); } } else { if (ParentList) { ParentList->GetSelection(l); } else if (Select()) { l.Insert(this); } } return l.Length() > 0; } void Mail::GetThread(List &Thread) { MContainer *c; for (c=Container; c->Parent; c=c->Parent); List Stack; Stack.Insert(c); while ((c=Stack[0])) { Stack.Delete(c); for (unsigned i=0; iChildren.Length(); i++) Stack.Insert(c->Children[i]); if (c->Message && !Thread.HasItem(c->Message)) { Thread.Insert(c->Message); } } } void Mail::SetListRead(bool Read) { List Sel; if (!_GetListItems(Sel, false)) return; LArray a; for (auto t: Sel) { auto m = dynamic_cast(t); if (!m) continue; bool isRead = (m->GetFlags() & MAIL_READ) != 0; if (Read ^ isRead) a.Add(m->GetObject()); } if (!a.Length()) return; auto Store = a[0]->GetStore(); if (!Store) { LAssert(0); return; } LVariant read = MAIL_READ; Store->Change(a, FIELD_FLAGS, read, Read ? OpPlusEquals : OpMinusEquals); } void SetFolderCallback(LInput *Dlg, LViewI *EditCtrl, void *Param) { ScribeWnd *App = (ScribeWnd*) Param; auto Str = EditCtrl->Name(); auto Select = new FolderDlg(Dlg, App, MAGIC_MAIL, 0, Str); Select->DoModal([&](auto dlg, auto id) { if (id) EditCtrl->Name(Select->Get()); delete dlg; }); } void Mail::DoContextMenu(LMouse &m, LView *p) { #ifdef _DEBUG LAutoPtr Prof(new LProfile("Mail::DoContextMenu")); Prof->HideResultsIfBelow(100); #endif if (!p) p = Parent; // open the right click menu MarkedState MarkState = MS_None; // Pre-processing for marks List Sel; if (_GetListItems(Sel, false)) { int Marked = 0; for (auto t: Sel) { Mail *m = dynamic_cast(t); if (m) { if (m->GetMarkColour()) { Marked++; } } } if (Marked == 1) { MarkState = MS_One; } else if (Marked) { MarkState = MS_Multiple; } } #ifdef _DEBUG Prof->Add("CreateMenu"); #endif // Create menu LScriptUi s(new LSubMenu); if (s.Sub) { /* Keyboard shortcuts: o - Open d - Delete x - Export e - Set read u - Set unread r - Reply a - Reply all f - Forward b - Bounce m - Mark s - Select marked c - Create filter i - Inspect p - Properties */ LMenuItem *i; s.Sub->SetImageList(App->GetIconImgList(), false); s.Sub->AppendItem(LLoadString(IDS_OPEN), IDM_OPEN, true); i = s.Sub->AppendItem(LLoadString(IDS_DELETE), IDM_DELETE, true); i->Icon(ICON_TRASH); s.Sub->AppendItem(LLoadString(IDS_EXPORT), IDM_EXPORT, true); int AttachBaseMsg = 10000; #ifdef _DEBUG Prof->Add("GetAttachments"); #endif List AttachLst; if (GetAttachments(&AttachLst) && AttachLst[0]) { LSubMenu *Attachments = s.Sub->AppendSub(LLoadString(IDS_SAVE_ATTACHMENT_AS)); if (Attachments) { int n=0; for (auto a: AttachLst) { auto Name = a->GetName(); Attachments->AppendItem(Name?Name:(char*)"Attachment.txt", AttachBaseMsg+(n++), true); } } } s.Sub->AppendSeparator(); #ifdef _DEBUG Prof->Add("Read/Unread"); #endif i = s.Sub->AppendItem(AddAmp(LLoadString(IDS_SET_READ), 'e'), IDM_SET_READ, true); i->Icon(ICON_READ_MAIL); i = s.Sub->AppendItem(AddAmp(LLoadString(IDS_SET_UNREAD), 'u'), IDM_SET_UNREAD, true); i->Icon(ICON_UNREAD_MAIL); s.Sub->AppendSeparator(); #ifdef _DEBUG Prof->Add("Reply/Bounce"); #endif i = s.Sub->AppendItem(AddAmp(LLoadString(IDS_REPLY), 'r'), IDM_REPLY, true); i->Icon(ICON_FLAGS_REPLY); s.Sub->AppendItem(AddAmp(LLoadString(IDS_REPLYALL), 'a'), IDM_REPLY_ALL, true); i = s.Sub->AppendItem(AddAmp(LLoadString(IDS_FORWARD), 'f'), IDM_FORWARD, true); i->Icon(ICON_FLAGS_FORWARD); i = s.Sub->AppendItem(AddAmp(LLoadString(IDS_BOUNCE), 'b'), IDM_BOUNCE, true); i->Icon(ICON_FLAGS_BOUNCE); s.Sub->AppendSeparator(); #ifdef _DEBUG Prof->Add("Thread"); #endif if (App->GetCtrlValue(IDM_THREAD)) { auto Thread = s.Sub->AppendSub(LLoadString(IDS_THREAD)); if (Thread) { Thread->AppendItem(LLoadString(IDS_THREAD_SELECT), IDM_SELECT_THREAD, true); Thread->AppendItem(LLoadString(IDS_THREAD_DELETE), IDM_DELETE_THREAD, true); Thread->AppendItem(LLoadString(IDS_THREAD_IGNORE), IDM_IGNORE_THREAD, true); s.Sub->AppendSeparator(); } } #ifdef _DEBUG Prof->Add("Mark"); #endif auto MarkMenu = s.Sub->AppendSub(AddAmp(LLoadString(IDS_MARK), 'm')); if (MarkMenu) { BuildMarkMenu(MarkMenu, MarkState, (uint32_t)GetMarkColour(), true); } MarkMenu = s.Sub->AppendSub(AddAmp(LLoadString(IDS_SELECT_MARKED), 's')); if (MarkMenu) { BuildMarkMenu(MarkMenu, MS_Multiple, 0, true, true, true); } s.Sub->AppendSeparator(); s.Sub->AppendItem(AddAmp(LLoadString(IDS_INSPECT), 'i'), IDM_INSPECT, true); s.Sub->AppendItem(LLoadString(IDS_PROPERTIES), IDM_PROPERTIES, true); #ifdef _DEBUG Prof->Add("ScriptCallbacks"); #endif LArray Callbacks; if (App->GetScriptCallbacks(LThingContextMenu, Callbacks)) { LScriptArguments Args(NULL); Args[0] = new LVariant(App); Args[1] = new LVariant(this); Args[2] = new LVariant(&s); for (auto c: Callbacks) App->ExecuteScriptCallback(*c, Args); Args.DeleteObjects(); } m.ToScreen(); int Result; #ifdef _DEBUG Prof.Reset(); #endif int Btn = 0; if (m.Left()) Btn = LSubMenu::BtnLeft; else if (m.Right()) Btn = LSubMenu::BtnRight; Result = s.Sub->Float(p, m.x, m.y); switch (Result) { case IDM_OPEN: { DoUI(); break; } case IDM_DELETE: { LVariant ConfirmDelete = false; App->GetOptions()->GetValue(OPT_ConfirmDelete, ConfirmDelete); if (!ConfirmDelete.CastInt32() || LgiMsg(GetList(), LLoadString(IDS_DELETE_ASK), AppName, MB_YESNO) == IDYES) { if (_GetListItems(Sel, false)) { int Index = -1; LList *TheList = LListItem::Parent; LArray Items; for (auto s: Sel) { Mail *m = dynamic_cast(s); if (m) { if (Index < 0) Index = TheList->IndexOf(m); Items.Add(m); } } GetFolder()->Delete(Items, true); if (Index >= 0) { LListItem *i = TheList->ItemAt(Index); if (i) i->Select(true); } } } break; } case IDM_EXPORT: { ExportAll(Parent, sMimeMessage, NULL); break; } case IDM_REPLY: case IDM_REPLY_ALL: { App->MailReplyTo(this, Result == IDM_REPLY_ALL); SetDirty(false); break; } case IDM_FORWARD: { App->MailForward(this); SetDirty(false); break; } case IDM_BOUNCE: { App->MailBounce(this); SetDirty(false); break; } case IDM_SET_READ: { SetListRead(true); break; } case IDM_SET_UNREAD: { SetListRead(false); break; } case IDM_INSPECT: { OnInspect(); break; } case IDM_PROPERTIES: { OnProperties(); break; } case IDM_SELECT_THREAD: { if (_GetListItems(Sel, false)) { List Thread; for (auto s: Sel) { Mail *m = dynamic_cast(s); if (m) m->GetThread(Thread); } for (auto m: Thread) { m->Select(true); } } break; } case IDM_DELETE_THREAD: { if (_GetListItems(Sel, false)) { List Thread; for (auto s: Sel) { Mail *m = dynamic_cast(s); if (m) m->GetThread(Thread); } for (auto m: Thread) { m->OnDelete(); } } break; } case IDM_IGNORE_THREAD: { if (_GetListItems(Sel, false)) { List Thread; for (auto s: Sel) { Mail *m = dynamic_cast(s); if (m) m->GetThread(Thread); } for (auto m: Thread) { m->SetFlags(m->GetFlags() | MAIL_IGNORE | MAIL_READ); } } break; } case IDM_UNMARK: case IDM_SELECT_NONE: case IDM_SELECT_ALL: default: { if (Result == IDM_UNMARK || (Result >= IDM_MARK_BASE && Result < IDM_MARK_BASE+CountOf(MarkColours32))) { bool Marked = Result != IDM_UNMARK; if (_GetListItems(Sel, false)) { COLOUR Col32 = Marked ? MarkColours32[Result - IDM_MARK_BASE] : 0; for (auto s: Sel) { Mail *m = dynamic_cast(s); if (m) { if (m->SetMarkColour(Col32)) { if (m->GetObject()->GetInt(FIELD_STORE_TYPE) != Store3Imap) // Imap knows to save itself. m->SetDirty(); } m->Update(); } } } } else if (Result == IDM_SELECT_NONE || Result == IDM_SELECT_ALL || (Result >= IDM_MARK_SELECT_BASE && Result < IDM_MARK_SELECT_BASE+CountOf(MarkColours32))) { bool None = Result == IDM_SELECT_NONE; bool All = Result == IDM_SELECT_ALL; uint32_t c32 = MarkColours32[Result - IDM_MARK_SELECT_BASE]; if (_GetListItems(Sel, true)) { for (auto s: Sel) { Mail *m = dynamic_cast(s); if (!m) break; auto CurCol = m->GetMarkColour(); if (None) { m->Select(CurCol <= 0); } else { if (CurCol > 0) { if (All) { m->Select(true); } else { m->Select(CurCol && CurCol == c32); } } else { m->Select(false); } } } } } else if (Result >= AttachBaseMsg && Result < AttachBaseMsg + (ssize_t)AttachLst.Length()) { // save attachment as... Attachment *a = AttachLst.ItemAt(Result - AttachBaseMsg); if (a) { a->OnSaveAs(Parent); } } else { // Handle any installed callbacks for menu items for (unsigned i=0; iExecuteScriptCallback(Cb, Args); } } } break; } } DeleteObj(s.Sub); } } void Mail::OnMouseClick(LMouse &m) { if (m.Down()) { if (!m.IsContextMenu()) { if (m.Double()) { // open the UI for the Item DoUI(); } } else { DoContextMenu(m); } } } void Mail::OnCreate() { LOptionsFile *Options = App->GetOptions(); LVariant v; if (GetObject()) GetObject()->SetInt(FIELD_FLAGS, MAIL_CREATED); // Get identity and set from info ScribeAccount *Ident = App->GetCurrentAccount(); if (Ident) { v = Ident->Identity.Name(); GetFrom()->SetStr(FIELD_NAME, v.Str()); v = Ident->Identity.Email(); GetFrom()->SetStr(FIELD_EMAIL, v.Str()); v = Ident->Identity.ReplyTo(); if (v.Str()) GetReply()->SetStr(FIELD_REPLY, v.Str()); } else { LAssert(!"No identity selected"); } LVariant EditCtrl; App->GetOptions()->GetValue(OPT_EditControl, EditCtrl); LAutoString Sig = GetSig(EditCtrl.CastInt32() != 0, Ident); if (!Sig && EditCtrl.CastInt32()) { Sig = GetSig(false, Ident); SetBody(Sig); } else if (EditCtrl.CastInt32()) SetHtml(Sig); else SetBody(Sig); LVariant ClipRecip; if (Options->GetValue(OPT_RecipientFromClipboard, ClipRecip) && ClipRecip.CastInt32()) { LClipBoard Clip(App); char *Txt = Clip.Text(); if (Txt && strchr(Txt, '@') && strlen(Txt) < 100) { for (char *s=Txt; *s; s++) { if (*s == '\n' || *s == '\r') { *s = 0; } } Mailto mt(App, Txt); for (auto a: mt.To) { LDataPropI *OldA = dynamic_cast(a); if (OldA) { LDataPropI *NewA = GetTo()->Create(GetObject()->GetStore()); if (NewA) { NewA->CopyProps(*OldA); GetTo()->Insert(NewA); } } } mt.To.Empty(); if (mt.Subject && !ValidStr(GetSubject())) { SetSubject(mt.Subject); } /* if (_strnicmp(Txt, MailToStr, 7) == 0) { char *Question = strchr(Txt + 7, '?'); if (Question) { *Question = 0; Question++; int Len = strlen(SubjectStr); if (_strnicmp(Question, SubjectStr, Len) == 0) { Subject = NewStr(Question + Len); } } La->Addr = NewStr(Txt + 7); } else { La->Addr = NewStr(Txt); } To.Insert(La); */ } } Update(); } void Mail::CreateMailHeaders() { LStringPipe Hdrs(256); MailProtocol Protocol; LVariant HideId; App->GetOptions()->GetValue(OPT_HideId, HideId); Protocol.ProgramName = GetFullAppName(!HideId.CastInt32()); LDataI *Obj = GetObject(); if (Obj && ::CreateMailHeaders(App, Hdrs, Obj, &Protocol)) { LAutoString FullHdrs(Hdrs.NewStr()); Obj->SetStr(FIELD_INTERNET_HEADER, FullHdrs); } else LAssert(!"CreateMailHeaders failed."); } bool Mail::OnBeforeSend(ScribeEnvelope *Out) { if (!Out) { LAssert(!"No output envelope."); return false; } // First check the email from address... if (!ValidStr(GetFrom()->GetStr(FIELD_EMAIL))) { LOptionsFile *Options = App->GetOptions(); if (Options) { ScribeAccount *Ident = App->GetAccounts()->ItemAt(App->GetCurrentIdentity()); if (Ident) { LVariant v = Ident->Identity.Email(); GetFrom()->SetStr(FIELD_EMAIL, v.Str()); v = Ident->Identity.Name(); GetFrom()->SetStr(FIELD_NAME, v.Str()); } } } Out->From = GetFromStr(FIELD_EMAIL); - GDataIt To = GetTo(); + LDataIt To = GetTo(); ContactGroup *Group = NULL; for (LDataPropI *t = To->First(); t; t = To->Next()) { LString Addr = t->GetStr(FIELD_EMAIL); if (LIsValidEmail(Addr)) Out->To.New() = Addr; else if ((Group = LookupContactGroup(App, Addr))) { LString::Array a = Group->GetAddresses(); Out->To.Add(a); } } LDataPropI *Root = GetObject()->GetObj(FIELD_MIME_SEG); if (!Root) { LAssert(!"No root element."); return false; } // Check the headers have been created.. if (!Root->GetStr(FIELD_INTERNET_HEADER)) CreateMailHeaders(); Out->MsgId = GetMessageId(true); // Convert the mime stream LMime Mime(ScribeTempPath()); LTempStream Buf(ScribeTempPath()); Store3ToGMime(&Mime, Root); // Do the encode if (!Mime.Text.Encode.Push(&Buf)) { LAssert(!"Mime encode failed."); return false; } Out->References = GetReferences(); Out->FwdMsgId = GetFwdMsgId(); Out->BounceMsgId = GetBounceMsgId(); // Read the resulting string into the output envelope int Sz = (int)Buf.GetSize(); Out->Rfc822.Length(Sz); Buf.SetPos(0); Buf.Read(Out->Rfc822.Get(), Sz); Out->Rfc822.Get()[Sz] = 0; return true; } void Mail::OnAfterSend() { int f = GetFlags(); f |= MAIL_SENT | MAIL_READ; // set sent flag f &= ~MAIL_READY_TO_SEND; // clear read to send flag // LgiTrace("Setting flags: %x\n", f); SetFlags(f); LDateTime n; n.SetNow(); SetDateSent(&n); // LString s = GetDateSent()->Get(); // LgiTrace("Setting sent date: %s\n", s.Get()); Update(); SetDirty(); if (App) { ScribeFolder *Sent = App->GetFolder(FOLDER_SENT, GetObject()); if (Sent) { LArray Items; Items.Add(this); Sent->MoveTo(Items); } } } bool Mail::OnBeforeReceive() { return true; } enum MimeDecodeMode { MODE_FIELDS, MODE_WAIT_MSG, MODE_SEGMENT, MODE_WAIT_TYPE, MODE_WAIT_BOUNDRY }; #define MF_UNKNOWN 1 #define MF_SUBJECT 2 #define MF_TO 3 #define MF_FROM 4 #define MF_REPLY_TO 5 #define MF_CONTENT_TYPE 6 #define MF_CC 7 #define MF_CONTENT_TRANSFER_ENCODING 9 #define MF_DATE_SENT 10 #define MF_PRIORITY 11 #define MF_COLOUR 12 #define MF_DISPOSITIONNOTIFICATIONTO 13 void MungCharset(Mail *Msg, bool &HasRealCs, ScribeAccount *Acc) { if (ValidStr(Msg->GetBodyCharset())) { HasRealCs = true; } if (Acc) { // LCharset *Cs = 0; if (Msg->GetBodyCharset() && stristr(Msg->GetBodyCharset(), "ascii") && Acc->Receive.AssumeAsciiCharset().Str()) { Msg->SetBodyCharset(Acc->Receive.AssumeAsciiCharset().Str()); HasRealCs = false; } else if (!ValidStr(Msg->GetBodyCharset()) || !LGetCsInfo(Msg->GetBodyCharset())) { Msg->SetBodyCharset(Acc->Receive.Assume8BitCharset().Str()); HasRealCs = false; } } } bool Mail::OnAfterReceive(LStreamI *Msg) { if (!Msg) return false; // Clear the codepage setting here so that we can // check later, down the bottom we must set it to // the default if it's not set anywhere along the // way. SetBodyCharset(0); if (GetObject() && !GetObject()->SetRfc822(Msg)) { LAssert(!"Failed to set mime content."); return false; } // Now parse them into the meta fields... GetObject()->ParseHeaders(); ScribeAccount *Acc = GetAccountSentTo(); LAutoString ContentType; // Fill out any Contact's TimeZone if missing LAutoString DateStr(InetGetHeaderField(GetInternetHeader(), "Date")); LDateTime DateObj; if (DateStr && DateObj.Decode(DateStr)) { double SenderTz = DateObj.GetTimeZoneHours(); if (SenderTz != 0.0) { ListAddr *Sender = dynamic_cast(GetFrom()); if (Sender) { auto It = Sender->begin(); if (*It) { Contact *c = (*It)->GetContact(); if (c) { const char *Tz = ""; if (!c->Get(OPT_TimeZone, Tz) || atof(Tz) != SenderTz) { char s[32]; sprintf_s(s, sizeof(s), "%.1f", SenderTz); c->Set(OPT_TimeZone, s); c->SetDirty(true); } } } } } } auto BodyText = GetBody(); if (BodyText) { if (!GetBodyCharset()) { if (Acc && Acc->Receive.Assume8BitCharset().Str()) { SetBodyCharset(Acc->Receive.Assume8BitCharset().Str()); } else { SetBodyCharset("us-ascii"); } } LStringPipe NewBody; LArray Files; if (DecodeUuencodedAttachment(GetObject()->GetStore(), Files, &NewBody, BodyText)) { LDataI *AttachPoint = GetFileAttachPoint(); if (AttachPoint) { for (unsigned i=0; iSetStr(FIELD_MIME_TYPE, sAppOctetStream); Files[i]->Save(AttachPoint); } SetDirty(!TestFlag(GetFlags(), MAIL_ATTACHMENTS)); SetFlags(GetFlags() | MAIL_ATTACHMENTS); LAutoString n(NewBody.NewStr()); SetBody(n); Update(); if (Ui) Ui->OnAttachmentsChange(); } } } LDateTime n; n.SetNow(); SetDateReceived(&n); Update(); SetDirty(); // Call any 'after receive' callbacks LArray Callbacks; if (App->GetScriptCallbacks(LMailOnAfterReceive, Callbacks)) { for (auto c: Callbacks) { if (!c->Func) continue; LVirtualMachine Vm; LScriptArguments Args(&Vm); Args.New() = new LVariant(App); Args.New() = new LVariant(this); App->ExecuteScriptCallback(*c, Args); Args.DeleteObjects(); } } return true; } ThingUi *Mail::GetUI() { return Ui; } LVariant Mail::GetServerUid() { LVariant v; if (GetObject()) { auto Type = (Store3Backend)GetObject()->GetInt(FIELD_STORE_TYPE); if (Type == Store3Imap) v = GetObject()->GetInt(FIELD_SERVER_UID); else v = GetObject()->GetStr(FIELD_SERVER_UID); } else LAssert(!"No object."); return v; } bool Mail::SetServerUid(LVariant &v) { if (!GetObject()) return false; Store3Status s; if (GetObject()->GetInt(FIELD_STORE_TYPE) == Store3Imap) s = GetObject()->SetInt(FIELD_SERVER_UID, v.CastInt32()); else s = GetObject()->SetStr(FIELD_SERVER_UID, v.Str()); return s > Store3Error; } bool Mail::SetObject(LDataI *o, bool IsDestructor, const char *File, int Line) { bool b = LDataUserI::SetObject(o, IsDestructor, File, Line); if (b) { // Clear out stale attachment objects... UnloadAttachments(); } return b; } bool Mail::SetUI(ThingUi *new_ui) { MailUi *NewMailUi = dynamic_cast(new_ui); if (NewMailUi == Ui) return true; if (Ui) { LWindow *w = Ui; Ui->SetItem(0); w->Quit(); LAssert(Ui == NULL); } if (new_ui) { Ui = dynamic_cast(new_ui); if (Ui) Ui->SetItem(this); else LAssert(!"Incorrect object."); } return true; } ThingUi *Mail::DoUI(MailContainer *c) { if (App && !Ui) { if (App->InThread()) { Ui = new MailUi(this, c ? c : GetFolder()); } else { App->PostEvent(M_SCRIBE_OPEN_THING, (LMessage::Param)this, 0); } } if (Ui) { #if WINNATIVE SetActiveWindow(Ui->Handle()); #endif } return Ui; } int Mail::Compare(LListItem *t, ssize_t Field) { Thing *T = (Thing*)t->_UserPtr; Mail *m = T ? T->IsMail() : 0; if (m) { static int Fields[] = {FIELD_FROM, FIELD_SUBJECT, FIELD_SIZE, FIELD_DATE_RECEIVED}; if (Field < 0) { auto i = -Field - 1; if (i >= 0 && i < CountOf(Fields)) { Field = Fields[i]; } } LVariant v1, v2; const char *s1 = "", *s2 = ""; switch (Field) { case 0: { break; } case FIELD_TO: { LDataPropI *a1 = GetTo()->First(); if (a1) { if (a1->GetStr(FIELD_NAME)) s1 = a1->GetStr(FIELD_NAME); else if (a1->GetStr(FIELD_EMAIL)) s1 = a1->GetStr(FIELD_EMAIL); } LDataPropI *a2 = m->GetTo()->First(); if (a2) { if (a2->GetStr(FIELD_NAME)) s2 = a2->GetStr(FIELD_NAME); else if (a2->GetStr(FIELD_EMAIL)) s2 = a2->GetStr(FIELD_EMAIL); } break; } case FIELD_FROM: { LDataPropI *f1 = GetFrom(); LDataPropI *f2 = m->GetFrom(); if (f1->GetStr(FIELD_NAME)) s1 = f1->GetStr(FIELD_NAME); else if (f1->GetStr(FIELD_EMAIL)) s1 = f1->GetStr(FIELD_EMAIL); if (f2->GetStr(FIELD_NAME)) s2 = f2->GetStr(FIELD_NAME); else if (f2->GetStr(FIELD_EMAIL)) s2 = f2->GetStr(FIELD_EMAIL); break; } case FIELD_SUBJECT: { s1 = GetSubject(); s2 = m->GetSubject(); break; } case FIELD_SIZE: { return (int) (TotalSizeof() - m->TotalSizeof()); break; } case FIELD_DATE_SENT: { auto Sent1 = GetDateSent(); auto Sent2 = m->GetDateSent(); if (!Sent1 || !Sent2) break; return Sent1->Compare(Sent2); break; } case FIELD_DATE_RECEIVED: { return GetDateReceived()->Compare(m->GetDateReceived()); break; } case FIELD_PRIORITY: { return GetPriority() - m->GetPriority(); break; } case FIELD_FLAGS: { int Mask = MAIL_FORWARDED | MAIL_REPLIED | MAIL_READ; return (GetFlags() & Mask) - (m->GetFlags() & Mask); break; } case FIELD_LABEL: { if (GetLabel()) s1 = GetLabel(); if (m->GetLabel()) s2 = m->GetLabel(); break; } case FIELD_MESSAGE_ID: { s1 = GetMessageId(); s2 = m->GetMessageId(); break; } case FIELD_FROM_CONTACT_NAME: { s1 = GetFieldText(FIELD_FROM_CONTACT_NAME); s2 = m->GetFieldText(FIELD_FROM_CONTACT_NAME); break; } case FIELD_SERVER_UID: { v1 = GetServerUid(); v2 = m->GetServerUid(); if (v1.Str() && v2.Str()) { s1 = v1.Str(); s2 = v2.Str(); } else { auto diff = v1.CastInt64() - v2.CastInt64(); if (diff < 0) return -1; return diff > 1; } } default: { s1 = GetObject() ? GetObject()->GetStr((int)Field) : 0; s2 = m->GetObject() ? m->GetObject()->GetStr((int)Field) : 0; break; } } const char *Empty = ""; return _stricmp(s1?s1:Empty, s2?s2:Empty); } return -1; } class MailPropDlg : public LDialog { List Lst; LViewI *Table = NULL; struct FlagInfo { int Flag = 0, Ctrl = 0; void Set(int f, int c) { Flag = f; Ctrl = c; } }; LArray Flags; public: MailPropDlg(LView *Parent, List &lst) { SetParent(Parent); Lst = lst; Flags.New().Set(MAIL_SENT, IDC_SENT); Flags.New().Set(MAIL_RECEIVED, IDC_RECEIVED); Flags.New().Set(MAIL_CREATED, IDC_CREATED); Flags.New().Set(MAIL_FORWARDED, IDC_FORWARDED); Flags.New().Set(MAIL_REPLIED, IDC_REPLIED); Flags.New().Set(MAIL_ATTACHMENTS, IDC_HAS_ATTACH); Flags.New().Set(MAIL_READ, IDC_READ); Flags.New().Set(MAIL_READY_TO_SEND, IDC_READY_SEND); if (LoadFromResource(IDD_MAIL_PROPERTIES)) { GetViewById(IDC_TABLE, Table); LAssert(Table != NULL); SetCtrlEnabled(IDC_OPEN_INSPECTOR, Lst.Length() == 1); for (auto &i: Flags) { int Set = 0; for (auto m: Lst) { if (m->GetFlags() & i.Flag) Set++; } if (Set == Lst.Length()) { SetCtrlValue(i.Ctrl, LCheckBox::CheckOn); } else if (Set) { LCheckBox *Cb; if (GetViewById(i.Ctrl, Cb)) Cb->ThreeState(true); SetCtrlValue(i.Ctrl, LCheckBox::CheckPartial); } } SetCtrlEnabled(IDC_HAS_ATTACH, false); char Msg[512] = ""; if (Lst.Length() == 1) { Mail *m = Lst[0]; auto mObj = m->GetObject(); if (mObj) { auto dt = mObj->GetDate(FIELD_DATE_RECEIVED); sprintf_s( Msg, sizeof(Msg), LLoadString(IDS_MAIL_PROPS_DLG), LFormatSize(mObj->GetInt(FIELD_SIZE)).Get(), dt ? dt->Get().Get() : LLoadString(IDS_NONE), mObj->GetStr(FIELD_DEBUG)); SetCtrlName(IDC_MSG_ID, mObj->GetStr(FIELD_MESSAGE_ID)); } } else { sprintf_s(Msg, sizeof(Msg), LLoadString(IDS_MULTIPLE_ITEMS), Lst.Length()); SetCtrlEnabled(IDC_MSG_ID, false); } SetCtrlName(IDC_MAIL_DESCRIPTION, Msg); MoveSameScreen(Parent); } } void OnPosChange() { if (Table) { LRect r = GetClient(); r.Inset(LTableLayout::CellSpacing, LTableLayout::CellSpacing); Table->SetPos(r); } } int OnNotify(LViewI *Ctr, LNotification n) { switch (Ctr->GetId()) { case IDC_OPEN_INSPECTOR: { if (Lst.Length()) Lst[0]->OnInspect(); break; } case IDOK: { for (auto &i: Flags) { auto v = GetCtrlValue(i.Ctrl); LAssert(v >= 0); // the control couldn't be found... check the .lr8 file for (auto m: Lst) { if (v == 1) m->SetFlags(m->GetFlags() | i.Flag); else if (v == 0) m->SetFlags(m->GetFlags() & ~i.Flag); } } // fall thru } case IDCANCEL: { EndModal(Ctr->GetId()); break; } } return 0; } }; void Mail::OnInspect() { new ObjectInspector(App, this); } void Mail::OnProperties(int Tab) { List Sel; if (GetList()->GetSelection(Sel)) { List Lst; for (auto i: Sel) { Lst.Insert(dynamic_cast(i)); } if (Lst[0]) { auto Dlg = new MailPropDlg(GetList(), Lst); Dlg->DoModal([this, Dlg](auto dlg, auto id) { if (id == IDOK) { SetDirty(); Update(); } delete dlg; }); } } } int Mail::GetImage(int SelFlags) { if (TestFlag(GetFlags(), MAIL_READY_TO_SEND) && !TestFlag(GetFlags(), MAIL_SENT)) { return ICON_UNSENT_MAIL; } if (GetFlags() & MAIL_READ) { return (GetFlags() & MAIL_ATTACHMENTS) ? ICON_READ_ATT_MAIL : ICON_READ_MAIL; } else { return (GetFlags() & MAIL_ATTACHMENTS) ? ICON_UNREAD_ATT_MAIL : ICON_UNREAD_MAIL; } return ICON_READ_MAIL; } int *Mail::GetDefaultFields() { return DefaultMailFields; } void Mail::DeleteAsSpam(LView *View) { // Set read.. SetFlags(GetFlags() | MAIL_READ | MAIL_BAYES_SPAM, true); // Remove from NewMail NewMailLst.Delete(this); LVariant DeleteOnServer, DeleteAttachments, SetRead; auto Opts = App->GetOptions(); if (!Opts->GetValue(OPT_BayesDeleteOnServer, DeleteOnServer)) DeleteOnServer = false; if (!Opts->GetValue(OPT_BayesDeleteAttachments, DeleteAttachments)) DeleteAttachments = false; if (!Opts->GetValue(OPT_BayesSetRead, SetRead)) SetRead = false; if (DeleteOnServer.CastBool()) { // Tell the account it's spam... so that any further connects can // delete it off the server. ScribeAccount *a = GetAccountSentTo(); if (a) { auto ServerUid = GetServerUid(); if (ServerUid.Str()) { a->Receive.DeleteAsSpam(ServerUid.Str()); SetServerUid(ServerUid = NULL); } else { LAutoString Uid(InetGetHeaderField(GetInternetHeader(), "X-UIDL")); if (Uid) a->Receive.DeleteAsSpam(Uid); } } else { #if 0 // def _DEBUG if (LgiMsg(Ui?(LView*)Ui:(LView*)App, "Debug: GetAccountSentTo failed. Debug?", AppName, MB_YESNO) == IDYES) { LAssert(0); } #endif } } if (DeleteAttachments.CastBool()) { // Delete all attachments... they're useless and more than likely // just virii anyway. List Files; if (GetAttachments(&Files)) { for (auto a: Files) { DeleteAttachment(a); } Files.Empty(); } } if (SetRead.CastInt32() != 0) { SetFlags(GetFlags() | MAIL_READ); } // Move it to the spam folder if it exists. auto FolderPath = GetFolder()->GetPath(); LToken Parts(FolderPath, "/"); if (Parts.Length() == 0) LgiMsg(View, "Error: No folder path?", AppName); else { LString SpamPath; LString SpamLeaf = "Spam"; SpamPath.Printf("/%s/%s", Parts[0], SpamLeaf.Get()); ScribeFolder *Spam = App->GetFolder(SpamPath); if (!Spam) { LMailStore *Ms = App->GetMailStoreForPath(FolderPath); if (!Ms) { if (GetFolder()->GetObject()->GetInt(FIELD_STORE_TYPE) == Store3Imap) { Ms = App->GetDefaultMailStore(); if (Ms && Ms->Root) { Spam = Ms->Root->GetSubFolder(SpamLeaf); if (!Spam) Spam = Ms->Root->CreateSubDirectory(SpamLeaf, MAGIC_MAIL); } } else LgiMsg(View, "Error: Couldn't get mail store for '%s'.", AppName, MB_OK, FolderPath.Get()); } else Spam = Ms->Root->CreateSubDirectory("Spam", MAGIC_MAIL); } if (Spam && Spam != GetFolder()) { LArray Items; Items.Add(this); if (!Spam->MoveTo(Items)) { LgiMsg(View, "Error: Couldn't move email to spam folder.", AppName); } } } } void Mail::SetFlagsCache(int64_t NewFlags, bool IgnoreReceipt, bool UpdateScreen) { if (FlagsCache == NewFlags) return; DepthCheck Depth(d->InSetFlagsCache); bool Read1 = TestFlag(FlagsCache, MAIL_READ); bool Read2 = TestFlag(NewFlags, MAIL_READ); bool ChangeRead = Read1 ^ Read2; // LgiTrace("%p::SetFlagsCache: %s -> %s\n", this, EmailFlagsToStr(FlagsCache).Get(), EmailFlagsToStr(StoreFlags).Get()); FlagsCache = NewFlags; if (ChangeRead && App) { if (Read2) { // Becoming read List Objs; Objs.Insert(this); App->OnNewMail(&Objs, false); PreviewCache.DeleteObjects(); // Read receipt if (!IgnoreReceipt && !TestFlag(NewFlags, MAIL_CREATED) && TestFlag(NewFlags, MAIL_READ_RECEIPT)) { LAutoString Header(InetGetHeaderField(GetInternetHeader(), "Disposition-Notification-To")); if (Header) { LAutoString Name, Addr; DecodeAddrName(Header, Name, Addr, 0); if (Addr) { if (LgiMsg( Ui != 0 ? (LView*)Ui : (LView*)App, LLoadString(IDS_RECEIPT_ASK), AppName, MB_YESNO, GetSubject(), GetFrom()->GetStr(FIELD_NAME), GetFrom()->GetStr(FIELD_EMAIL), (char*)Name, (char*)Addr) == IDYES) { Mail *m = new Mail(App); if (m) { m->App = App; LDataPropI *ToAddr = m->GetTo()->Create(m->GetObject()->GetStore()); if (ToAddr) { ToAddr->SetStr(FIELD_NAME, Name); ToAddr->SetStr(FIELD_EMAIL, Addr); m->GetTo()->Insert(ToAddr); } m->OnReceipt(this); m->Save(); App->Send(); } } } } } LVariant Inc; if (App->GetOptions()->GetValue(OPT_BayesIncremental, Inc) && Inc.CastInt32()) { // Incremental bayesian update App->OnBayesianMailEvent(this, BayesMailUnknown, BayesMailHam); } } if (UpdateScreen) { // Changing read status ScribeFolder *t = GetFolder(); if (t) t->OnUpdateUnRead(0, true); } } if (UpdateScreen || ChangeRead) { Update(); } } uint32_t Mail::GetFlags() { if (GetObject()) { // Make sure the objects are in sync... auto StoreFlags = GetObject()->GetInt(FIELD_FLAGS); if (FlagsCache < 0) FlagsCache = StoreFlags; else if (FlagsCache != StoreFlags) SetFlagsCache(StoreFlags, false, true); } return (uint32_t)FlagsCache; } void Mail::SetFlags(ulong NewFlags, bool IgnoreReceipt, bool UpdateScreen) { if (FlagsCache == NewFlags) return; DepthCheck SetFlagsDepth(d->InSetFlags); if (GetObject()) { Store3Status Result = GetObject()->SetInt(FIELD_FLAGS, NewFlags); if (Result == Store3Success && GetObject()->IsOnDisk()) { // Imap mail shouldn't fall in here. SetDirty(); } } else { LAssert(0); return; } SetFlagsCache(NewFlags, IgnoreReceipt, UpdateScreen); } const char *Mail::GetFieldText(int Field) { static char Buf[512]; switch (Field) { case FIELD_TO: { size_t ch = 0; Buf[0] = 0; - GDataIt To = GetTo(); + LDataIt To = GetTo(); for (LDataPropI *a=To->First(); a; a=To->Next()) { if (ch > 0) ch += sprintf_s(Buf+ch, sizeof(Buf)-ch, ", "); auto Name = a->GetStr(FIELD_NAME); auto Addr = a->GetStr(FIELD_EMAIL); auto n = Name ? Name : Addr; if (!n) continue; // Is the buffer too small? size_t n_len = strlen(n); if (ch + n_len + 8 >= sizeof(Buf)) { // Yes... just truncate it with "..." and bail. ch += sprintf_s(Buf+ch, sizeof(Buf)-ch, "..."); break; } ch += sprintf_s(Buf+ch, sizeof(Buf)-ch, "%s", n); LAssert(ch < sizeof(Buf) - 1); } return Buf; break; } case FIELD_FROM_CONTACT_NAME: { static bool InMethod = false; if (!InMethod) { InMethod = true; LDataPropI *la = GetFrom(); if (la) { auto Email = la->GetStr(FIELD_EMAIL); Contact *c = Contact::LookupEmail(Email); if (c) { const char *f = 0, *l = 0; c->Get(OPT_First, f); c->Get(OPT_Last, l); if (f && l) sprintf_s(Buf, sizeof(Buf), "%s %s", f, l); else if (f) strcpy_s(Buf, sizeof(Buf), f); else if (l) strcpy_s(Buf, sizeof(Buf), l); else Buf[0] = 0; InMethod = false; return Buf; } } InMethod = false; } else { LAssert(0); } // fall through } case FIELD_FROM: { LDataPropI *f = GetFrom(); auto n = f->GetStr(FIELD_NAME); if (n) return n; auto e = f->GetStr(FIELD_EMAIL); if (e) return e; break; } case FIELD_SUBJECT: return GetSubject(); case FIELD_SIZE: { if (TotalSizeCache < 0) TotalSizeof(); if (OptionSizeInKiB) { int ch = sprintf_s(Buf, sizeof(Buf), "%.0f KiB", (double)TotalSizeCache/1024.0); if (ch > 4 + 3) { int digits = ch - 4; char *s = Buf + ((digits % 3) ? digits % 3 : 3); char *e = Buf + ch - 4; while (s < e) { memmove(s + 1, s, strlen(s)+1); *s = ','; s += 3; } } } else LFormatSize(Buf, sizeof(Buf), TotalSizeCache); return Buf; } case FIELD_DATE_SENT: { auto DateSent = GetDateSent(); if (DateSent->Year()) { LDateTime dt = *DateSent; if (GetObject() && GetObject()->GetInt(FIELD_STORE_TYPE) == Store3Imap) { ValidateImapDate(dt); } if (AdjustDateTz) { if (dt.GetTimeZone() != LDateTime::SystemTimeZone()) dt.SetTimeZone(LDateTime::SystemTimeZone(), true); } if (ShowRelativeDates) { auto rel = RelativeTime(dt); strcpy_s(Buf, sizeof(Buf), rel ? rel : "#error:RelativeTime"); } else { dt.Get(Buf, sizeof(Buf)); } } else { sprintf_s(Buf, sizeof(Buf), "(%s)", LLoadString(IDS_NONE, "None")); } return Buf; } case FIELD_DATE_RECEIVED: { auto DateReceived = GetDateReceived(); if (DateReceived->Year()) { LDateTime dt = *DateReceived; if (AdjustDateTz) { if (dt.GetTimeZone() != LDateTime::SystemTimeZone()) dt.SetTimeZone(LDateTime::SystemTimeZone(), true); } dt.Get(Buf, sizeof(Buf)); if (ShowRelativeDates) { auto rel = RelativeTime(dt); strcpy_s(Buf, sizeof(Buf), rel ? rel : "#error:RelativeTime"); } else { dt.Get(Buf, sizeof(Buf)); } } else { sprintf_s(Buf, sizeof(Buf), "(%s)", LLoadString(IDS_NONE, "None")); } return Buf; } case FIELD_TEXT: return GetBody(); case FIELD_MESSAGE_ID: return GetMessageId(); case FIELD_INTERNET_HEADER: return GetInternetHeader(); case FIELD_ALTERNATE_HTML: return GetHtml(); case FIELD_LABEL: return GetLabel(); case FIELD_PRIORITY: case FIELD_FLAGS: return 0; case FIELD_SERVER_UID: sprintf_s(Buf, sizeof(Buf), "%i", GetObject() ? (int)GetObject()->GetInt(Field) : -1); return Buf; case FIELD_RECEIVED_DOMAIN: { if (!d->DomainCache) { if (!GetObject()) return NULL; auto hdr = GetObject()->GetStr(FIELD_INTERNET_HEADER); if (!hdr) return NULL; for (auto ln: LString(hdr).SplitDelimit("\n")) { if (ln.Find("Received: from") == 0) { auto p = ln.SplitDelimit(); if (p.Length() > 2) { auto t = p[2]; if (t.Find(".") > 0) d->DomainCache = t.Strip("[]"); } } } } return d->DomainCache; } default: return GetObject() ? GetObject()->GetStr(Field) : NULL; } return 0; } int Mail::DefaultMailFields[] = { /* FIELD_FLAGS, FIELD_PRIORITY, */ FIELD_FROM, FIELD_SUBJECT, FIELD_SIZE, FIELD_DATE_SENT }; const char *Mail::GetText(int i) { if (FieldArray.Length()) { if (i >= 0 && i < (int)FieldArray.Length()) return GetFieldText(FieldArray[i]); } else if (i < CountOf(DefaultMailFields)) { return GetFieldText(DefaultMailFields[i]); } return NULL; } LDataI *Mail::GetFileAttachPoint() { if (!GetObject()) { LAssert(!"No object ptr."); return 0; } + + auto Store = GetObject()->GetStore(); // Check existing root node for "multipart/mixed"? - LDataI *r = dynamic_cast(GetObject()->GetObj(FIELD_MIME_SEG)); + auto r = dynamic_cast(GetObject()->GetObj(FIELD_MIME_SEG)); if (!r) { // Create one... - r = GetObject()->GetStore()->Create(MAGIC_ATTACHMENT); + r = Store->Create(MAGIC_ATTACHMENT); if (!r) { LAssert(!"No MIME segment ptr."); return NULL; } r->SetStr(FIELD_MIME_TYPE, sMultipartMixed); if (!GetObject()->SetObj(FIELD_MIME_SEG, r)) { LAssert(!"Can't set MIME segment."); return NULL; } } auto Mt = r->GetStr(FIELD_MIME_TYPE); - if (Mt && !_stricmp(Mt, sMultipartMixed)) + if (!Stricmp(Mt, sMultipartMixed)) { // Yes is it mixed... return that... return r; } - // No, a different type of segment, make the parent seg a "mixed", and attach the old segment to the mixed - LDataI *Mixed = GetObject()->GetStore()->Create(MAGIC_ATTACHMENT); + // No, a different type of segment, make the parent segment a "mixed", and attach the old segment to the mixed + auto Mixed = Store->Create(MAGIC_ATTACHMENT); if (!Mixed) { LAssert(!"Failed to create MAGIC_ATTACHMENT."); - return 0; + return NULL; } // Set the type... Mixed->SetStr(FIELD_MIME_TYPE, sMultipartMixed); - // Reparent the content to the mixed + // Re-parent the content to the mixed if (!r->Save(Mixed)) { LAssert(!"Can't reparent the content into the mixed seg."); - return 0; - } - - // Reparent the mixed to the mail object + return NULL; + } + + // Re-parent the mixed to the mail object if (!Mixed->Save(GetObject())) { LAssert(!"Can't reparent the mixed seg into the mail object."); - return 0; + return NULL; } return Mixed; } bool Mail::AttachFile(Attachment *File) { bool Status = false; if (File && !Attachments.HasItem(File)) { LDataI *AttachPoint = GetFileAttachPoint(); if (AttachPoint) { if (File->GetObject()->Save(AttachPoint)) { File->App = App; File->SetOwner(this); Attachments.Insert(File); Status = true; SetDirty(!TestFlag(GetFlags(), MAIL_ATTACHMENTS)); SetFlags(GetFlags() | MAIL_ATTACHMENTS); Update(); if (Ui) { Ui->OnAttachmentsChange(); } } } else { File->DecRef(); } } return Status; } Attachment *Mail::AttachFile(LView *Parent, const char *FileName) { LString MimeType = ScribeGetFileMimeType(FileName); LVariant Resize = false; - if (MimeType && !_strnicmp(MimeType, "image/", 6)) + if (!Strnicmp(MimeType.Get(), "image/", 6)) { // Check if we have to do a image resize App->GetOptions()->GetValue(OPT_ResizeImgAttachments, Resize); } - LDataI *AttachPoint = GetFileAttachPoint(); + auto AttachPoint = GetFileAttachPoint(); if (!AttachPoint) { LAssert(!"No attachment point in MIME heirarchy for file"); return NULL; } - Attachment *File = new Attachment( App, - GetObject()->GetStore()->Create(MAGIC_ATTACHMENT), - FileName); + auto File = new Attachment( App, + GetObject()->GetStore()->Create(MAGIC_ATTACHMENT), + FileName); if (File) { File->App = App; if (Resize.CastInt32() || File->GetSize() > 0) { File->SetOwner(this); Attachments.Insert(File); if (Resize.CastInt32()) d->AddResizeImage(File); else File->GetObject()->Save(AttachPoint); SetFlags(GetFlags() | MAIL_ATTACHMENTS); Update(); if (Ui) Ui->OnAttachmentsChange(); } else { LgiMsg(Parent, LLoadString(IDS_ERROR_FILE_EMPTY), AppName); File->DecRef(); File = NULL; } } return File; } bool Mail::Save(ScribeFolder *Into) { bool Status = false; if (!Into) { if (GetFolder()) Into = GetFolder(); else Into = App->GetFolder(FOLDER_OUTBOX); } if (Into) { ScribeFolder *Old = GetFolder(); if (!GetFolder()) { SetParentFolder(Into); } else if (GetFolder() != Into) { // If this fails, you should really by using ScribeFolder::MoveTo to move // the item from it's existing location to the new folder... LAssert(!"Use MoveTo instead."); return false; } Store3Status Result = Into->WriteThing(this); if (Result != Store3Error) { Status = true; SetDirty(false); if (Into && Into == App->GetFolder(FOLDER_TEMPLATES, NULL, true)) { // saving into the templates folder... update the menu App->BuildDynMenus(); } if (Status && DropFileName) { FileDev->Delete(DropFileName, false); DropFileName.Reset(); } } else { SetParentFolder(Old); } } // This frees the resizer thread... // so they aren't hanging around pointlessly d->OnSave(); return Status; } struct WrapQuoteBlock { int Depth; LArray Text; }; void WrapAndQuote( LStream &Pipe, const char *Quote, int Wrap, const char *Text, const char *Cp, const char *MimeType) { int IsHtml = MimeType && !_stricmp(MimeType, sTextHtml); LAutoString In((char*) LNewConvertCp("utf-8", Text, Cp ? Cp : (char*)"utf-8")); const char *QuoteStr = Quote; if (In) { size_t QuoteChars = Strlen(QuoteStr); char NewLine[8]; int NewLineLen = sprintf_s(NewLine, sizeof(NewLine), "%s", IsHtml ? "
\n" : "\n"); // Two step process, parse all the input into an array of paragraph blocks and then output each block // in wrapped form LArray Para; // 1) Parse all input into paragraphs char *i = In; while (*i) { // Parse out the next line... char *e = i; while (*e && *e != '\n') e++; ssize_t len = e - i; int depth = 0; for (int n=0; n') depth++; else if (i[n] != ' ' || i[n] != '\t') break; } if (Para.Length()) { // Can this be added to the previous paragraph? WrapQuoteBlock &Prev = Para[Para.Length()-1]; if (Prev.Depth == depth) { // Yes?! Prev.Text.Add(NewStr(i, len)); i = *e ? e + 1 : e; continue; } } // Create a new paragraph WrapQuoteBlock &New = Para.New(); New.Depth = depth; New.Text.Add(NewStr(i, len)); // Move current position to next line i = *e ? e + 1 : e; } // 2) Output all paragraphs const char *PrefixCharacters = "> \t"; for (unsigned n=0; n 0); if (WordLen == 0) break; // This won't end well... if (Wrap > 0) { // Check if we can put this word on the current line without making it longer than 'Wrap' if (CharPos + WordLen > Wrap) { // No it won't fit... so emit [NewLine][QuoteStr][Prefix] Pipe.Write(NewLine, NewLineLen); if (QuoteStr) Pipe.Write(QuoteStr, QuoteChars * sizeof(*QuoteStr)); Pipe.Write(Prefix, PrefixChars * sizeof(*Prefix)); // Now we are ready to write more words... CharPos = QuoteChars + PrefixChars; } // else Yes it fits... } // Write out the word... if (IsHtml && Strnchr(Start, '<', WordLen)) { for (auto *c = Start; c < Start + WordLen; c++) if (*c == '<') Pipe.Print("<"); else if (*c == '>') Pipe.Print(">"); else Pipe.Write(c, sizeof(*c)); } else Pipe.Write(Start, WordLen * sizeof(*Start)); CharPos += WordLen; Start = End; } if (HardNewLine) { Pipe.Write(NewLine, NewLineLen); if (QuoteStr) Pipe.Write(QuoteStr, QuoteChars * sizeof(*QuoteStr)); Pipe.Write(Prefix, PrefixChars * sizeof(*Prefix)); CharPos = QuoteChars + PrefixChars; } } // Finish the paragraph Pipe.Write(NewLine, NewLineLen); } } } void Mail::WrapAndQuote(LStringPipe &p, const char *Quote, int WrapAt) { LString Mem; LAutoString Cp; if (!ValidStr(GetBody())) Mem = HtmlToText(GetHtml(), GetHtmlCharset()); else Cp = GetCharSet(); ::WrapAndQuote(p, Quote, WrapAt > 0 ? WrapAt : 76, Mem ? Mem.Get() : GetBody(), Cp); } LAutoString Mail::GetSig(bool HtmlVersion, ScribeAccount *Account) { LAutoString r; LVariant Xml; if (!Account) Account = GetAccountSentTo(); if (Account) { if (HtmlVersion) Xml = Account->Identity.HtmlSig(); else Xml = Account->Identity.TextSig(); } if (Xml.Str()) { r = App->ProcessSig(this, Xml.Str(), HtmlVersion ? sTextHtml : sTextPlain); } return r; } void Mail::ProcessTextForResponse(Mail *From, LOptionsFile *Options, ScribeAccount *Account) { LStringPipe Temp; LVariant QuoteStr, Quote, WrapAt; Options->GetValue(OPT_QuoteReply, Quote); Options->GetValue(OPT_WrapAtColumn, WrapAt); Options->GetValue(OPT_QuoteReplyStr, QuoteStr); From->WrapAndQuote(Temp, Quote.CastInt32() ? QuoteStr.Str() : NULL, WrapAt.CastInt32()); LAutoString s = From->GetSig(false, Account); if (s) Temp.Write(s, strlen(s)); s.Reset(Temp.NewStr()); SetBody(s); SetBodyCharset("utf-8"); } ScribeAccount *Mail::GetAccountSentTo() { ScribeAccount *Account = App->GetAccountById(GetAccountId()); if (!Account) { // Older style lookup based on to address... for (auto a : *App->GetAccounts()) { LVariant e = a->Identity.Email(); if (ValidStr(e.Str())) { for (LDataPropI *t=GetTo()->First(); t; t=GetTo()->Next()) { auto Addr = t->GetStr(FIELD_EMAIL); if (ValidStr(Addr)) { if (_stricmp(Addr, e.Str()) == 0) { Account = a; break; } } } } } } return Account; } void Mail::OnReceipt(Mail *m) { if (m) { SetFlags(MAIL_CREATED | MAIL_READY_TO_SEND); ScribeAccount *MatchedAccount = m->GetAccountSentTo(); if (!MatchedAccount) { MatchedAccount = App->GetAccounts()->ItemAt(App->GetCurrentIdentity()); } if (MatchedAccount) { GetFrom()->SetStr(FIELD_NAME, MatchedAccount->Identity.Name().Str()); GetFrom()->SetStr(FIELD_EMAIL, MatchedAccount->Identity.Email().Str()); } char Sent[64]; m->GetDateReceived()->Get(Sent, sizeof(Sent)); char Read[64]; LDateTime t; t.SetNow(); t.Get(Read, sizeof(Read)); char s[256]; sprintf_s(s, sizeof(s), LLoadString(IDS_RECEIPT_BODY), GetFrom()->GetStr(FIELD_NAME), GetFrom()->GetStr(FIELD_EMAIL), Sent, m->GetSubject(), Read); SetBody(s); sprintf_s(s, sizeof(s), "Read: %s", m->GetSubject() ? m->GetSubject() : (char*)""); SetSubject(s); } } LString Mail::GetMailRef() { LString Ret; if (auto MsgId = GetMessageId(true)) { LAssert(!strchr(MsgId, '\n')); ScribeFolder *Fld = GetFolder(); LString FldPath; if (Fld) FldPath = Fld->GetPath(); if (FldPath) { LUri u; auto a = u.EncodeStr(MsgId, MsgIdEncodeChars); if (a) Ret.Printf("%s/%s", FldPath.Get(), a.Get()); } else { Ret = MsgId; } } return Ret; } void Mail::OnReply(Mail *m, bool All, bool MarkOriginal) { if (!m) return; SetWillDirty(false); LVariant ReplyAllSetting = MAIL_ADDR_TO; if (All) { App->GetOptions()->GetValue(OPT_DefaultReplyAllSetting, ReplyAllSetting); } if (MarkOriginal) { // source email has been replyed to m->SetFlags(m->GetFlags() | MAIL_READ); } // this email is ready to send SetFlags(GetFlags() | MAIL_READ | MAIL_CREATED); LDataPropI *ToAddr = GetTo()->Create(GetObject()->GetStore()); if (ToAddr) { bool CopyStatus; auto From = m->GetFrom(); auto Reply = m->GetReply(); auto ReplyTo = Reply->GetStr(FIELD_EMAIL); if (ReplyTo) CopyStatus = ToAddr->CopyProps(*Reply); else CopyStatus = ToAddr->CopyProps(*From); LAssert(CopyStatus); ToAddr->SetInt(FIELD_CC, ReplyAllSetting.CastInt32()); GetTo()->Insert(ToAddr); } LVariant UserName, EmailAddr, ReplyTo; ScribeAccount *a = App->GetCurrentAccount(); if (a) { UserName = a->Identity.Name(); EmailAddr = a->Identity.Email(); ReplyTo = a->Identity.ReplyTo(); } else LAssert(!"No current account."); if (All) { for (LDataPropI *a = m->GetTo()->First(); a; a = m->GetTo()->Next()) { bool Same = App->IsMyEmail(a->GetStr(FIELD_EMAIL)); bool AlreadyThere = false; for (LDataPropI *r = GetTo()->First(); r; r=GetTo()->Next()) { if (_stricmp(r->GetStr(FIELD_EMAIL), a->GetStr(FIELD_EMAIL)) == 0) { AlreadyThere = true; break; } } if (!Same && !AlreadyThere) { LDataPropI *New = GetTo()->Create(GetObject()->GetStore()); if (New) { New->SetInt(FIELD_CC, ReplyAllSetting.CastInt32()); New->SetStr(FIELD_EMAIL, a->GetStr(FIELD_EMAIL)); New->SetStr(FIELD_NAME, a->GetStr(FIELD_NAME)); GetTo()->Insert(New); } } } } ScribeAccount *MatchedAccount = m->GetAccountSentTo(); if (MatchedAccount && MatchedAccount->Identity.Email().Str()) { GetFrom()->SetStr(FIELD_NAME, MatchedAccount->Identity.Name().Str()); GetFrom()->SetStr(FIELD_EMAIL, MatchedAccount->Identity.Email().Str()); ReplyTo = MatchedAccount->Identity.ReplyTo(); if (ReplyTo.Str()) { GetReply()->SetStr(FIELD_NAME, MatchedAccount->Identity.Name().Str()); GetReply()->SetStr(FIELD_EMAIL, ReplyTo.Str()); } } else { GetFrom()->SetStr(FIELD_NAME, UserName.Str()); GetFrom()->SetStr(FIELD_EMAIL, EmailAddr.Str()); if (ValidStr(ReplyTo.Str())) { GetReply()->SetStr(FIELD_NAME, UserName.Str()); GetReply()->SetStr(FIELD_EMAIL, ReplyTo.Str()); } } char Re[32]; sprintf_s(Re, sizeof(Re), "%s:", LLoadString(IDS_REPLY_PREFIX)); auto SrcSubject = (m->GetSubject()) ? m->GetSubject() : ""; if (_strnicmp(SrcSubject, Re, strlen(Re)) != 0) { size_t Len = strlen(SrcSubject) + strlen(Re) + 2; char *s = new char[Len]; if (s) { sprintf_s(s, Len, "%s %s", Re, SrcSubject); SetSubject(s); DeleteArray(s); } } else { SetSubject(m->GetSubject()); } const char *EditMimeType = App->EditCtrlMimeType(); LAutoString Xml = App->GetReplyXml(EditMimeType); if (ValidStr(Xml)) { RemoveReturns(Xml); PreviousMail = m; LString Body = App->ProcessReplyForwardTemplate(m, this, Xml, Cursor, EditMimeType); LVariant EditCtrl; if (App->GetOptions()->GetValue(OPT_EditControl, EditCtrl) && EditCtrl.CastInt32()) { SetHtml(Body); } else { SetBody(Body); SetBodyCharset("utf-8"); } PreviousMail = 0; } else { ProcessTextForResponse(m, App->GetOptions(), MatchedAccount); } // Reference SetReferences(0); auto MailRef = m->GetMailRef(); if (MailRef) SetReferences(MailRef); GetMessageId(true); SetWillDirty(true); ScribeFolder *Folder = m->GetFolder(); if (Folder && Folder->IsPublicFolders()) { SetParentFolder(Folder); } } bool Mail::OnForward(Mail *m, bool MarkOriginal, int WithAttachments) { if (!m) { LAssert(!"No mail object."); LgiTrace("%s:%i - No mail object.\n", _FL); return false; } // ask about attachments? List Attach; if (m->GetAttachments(&Attach) && Attach.Length() > 0) { if (WithAttachments < 0) { LView *p = (m->Ui)?(LView*)m->Ui:(LView*)App; int Result = LgiMsg(p, LLoadString(IDS_ASK_FORWARD_ATTACHMENTS), AppName, MB_YESNOCANCEL); if (Result == IDYES) { WithAttachments = 1; } else if (Result == IDCANCEL) { return false; } } } if (MarkOriginal) { // source email has been forwarded auto MailRef = m->GetMailRef(); if (MailRef) SetFwdMsgId(MailRef); } // this email is ready to send SetFlags(GetFlags() | MAIL_CREATED); SetDirty(); LVariant UserName, EmailAddr, ReplyTo; ScribeAccount *a = App->GetCurrentAccount(); if (a) { UserName = a->Identity.Name(); EmailAddr = a->Identity.Email(); ReplyTo = a->Identity.ReplyTo(); } else LAssert(!"No current account."); ScribeAccount *MatchedAccount = m->GetAccountSentTo(); if (MatchedAccount && MatchedAccount->Identity.Email().Str()) { GetFrom()->SetStr(FIELD_NAME, MatchedAccount->Identity.Name().Str()); GetFrom()->SetStr(FIELD_EMAIL, MatchedAccount->Identity.Email().Str()); ReplyTo = MatchedAccount->Identity.ReplyTo(); if (ValidStr(ReplyTo.Str())) { GetReply()->SetStr(FIELD_NAME, MatchedAccount->Identity.Name().Str()); GetReply()->SetStr(FIELD_EMAIL, ReplyTo.Str()); } } else { GetFrom()->SetStr(FIELD_NAME, UserName.Str()); GetFrom()->SetStr(FIELD_EMAIL, EmailAddr.Str()); if (ValidStr(ReplyTo.Str())) { GetReply()->SetStr(FIELD_NAME, UserName.Str()); GetReply()->SetStr(FIELD_EMAIL, ReplyTo.Str()); } } SetBodyCharset(0); char PostFix[32]; sprintf_s(PostFix, sizeof(PostFix), "%s:", LLoadString(IDS_FORWARD_PREFIX)); auto SrcSubject = (m->GetSubject()) ? m->GetSubject() : ""; if (_strnicmp(SrcSubject, PostFix, strlen(PostFix)) != 0) { size_t Len = strlen(SrcSubject) + strlen(PostFix) + 2; char *s = new char[Len]; if (s) { sprintf_s(s, Len, "%s %s", PostFix, SrcSubject); SetSubject(s); DeleteArray(s); } } else { SetSubject(m->GetSubject()); } // Wrap/Quote/Sig the text... const char *EditMimeType = App->EditCtrlMimeType(); LAutoString Xml = App->GetForwardXml(EditMimeType); if (ValidStr(Xml)) { RemoveReturns(Xml); PreviousMail = m; LString Body = App->ProcessReplyForwardTemplate(m, this, Xml, Cursor, EditMimeType); LVariant EditCtrl; if (App->GetOptions()->GetValue(OPT_EditControl, EditCtrl) && EditCtrl.CastInt32()) { SetHtml(Body); } else { SetBody(Body); SetBodyCharset("utf-8"); } PreviousMail = 0; } else { ProcessTextForResponse(m, App->GetOptions(), MatchedAccount); } // Attachments if (WithAttachments > 0) for (auto From: Attach) AttachFile(new Attachment(App, From)); // Set MsgId GetMessageId(true); // Unless the user edits the message it shouldn't be made dirty. SetDirty(false); return true; } bool Mail::OnBounce(Mail *m, bool MarkOriginal, int WithAttachments) { if (m) { if (MarkOriginal) { // source email has been forwarded auto MsgId = m->GetMessageId(true); if (MsgId) { ScribeFolder *Fld = m->GetFolder(); LString FldPath; if (Fld) FldPath = Fld->GetPath(); if (FldPath) { LUri u; LString a = u.EncodeStr(MsgId, MsgIdEncodeChars); if (a) { LString p; p.Printf("%s/%s", FldPath.Get(), a.Get()); SetBounceMsgId(p); } } else { SetBounceMsgId(MsgId); } } else m->SetFlags(m->GetFlags() | MAIL_BOUNCED); } *this = (Thing&)*m; GetTo()->DeleteObjects(); SetFlags(MAIL_READ | MAIL_BOUNCE); return true; } return false; } void Mail::OnMeasure(LPoint *Info) { LListItem::OnMeasure(Info); if (PreviewLines && !(GetFlags() & MAIL_READ) && (!GetObject() || !GetObject()->GetInt(FIELD_DONT_SHOW_PREVIEW))) { LFont *PreviewFont = App->GetPreviewFont(); Info->y += PreviewFont ? PreviewFont->GetHeight() << 1 : 28; } } void Mail::OnPaintColumn(LItem::ItemPaintCtx &Ctx, int i, LItemColumn *c) { int Field = 0; if (FieldArray.Length()) { Field = i >= 0 && i < (int)FieldArray.Length() ? FieldArray[i] : 0; } else if (i >= 0 && i < CountOf(DefaultMailFields)) { Field = DefaultMailFields[i]; } if (Container && i >= 0 && Field == FIELD_SUBJECT) { LFont *f = Parent->GetFont(); auto Subj = GetSubject(); if (Container) { // Container needs to paint the subject... Container->OnPaint(Ctx.pDC, Ctx, c, Ctx.Fore, Ctx.Back, f?f:LSysFont, Subj); } } else { LListItem::OnPaintColumn(Ctx, i, c); if (i >= 0 && App->GetIconImgList()) { int Icon = -1; switch (Field) { case FIELD_PRIORITY: { switch (GetPriority()) { case MAIL_PRIORITY_HIGH: { Icon = ICON_PRIORITY_HIGH; break; } case MAIL_PRIORITY_LOW: { Icon = ICON_PRIORITY_LOW; break; } default: break; } break; } case FIELD_FLAGS: { if (GetFlags() & MAIL_BOUNCED) { Icon = ICON_FLAGS_BOUNCE; } else if (GetFlags() & MAIL_REPLIED) { Icon = ICON_FLAGS_REPLY; } else if (GetFlags() & MAIL_FORWARDED) { Icon = ICON_FLAGS_FORWARD; } break; } default: break; } if (Icon >= 0) { LRect *b = App->GetIconImgList()->GetBounds(); if (b) { b += Icon; int x = Ctx.x1 + ((Ctx.X()-b->X())/2) - b->x1; int y = Ctx.y1 + ((MIN(Ctx.Y(), 16)-b->Y())/2) - b->y1; LColour Back(Ctx.Back); App->GetIconImgList()->Draw(Ctx.pDC, x, y, Icon, Back); } } } } } void Mail::OnPaint(LItem::ItemPaintCtx &InCtx) { LItem::ItemPaintCtx Ctx = InCtx; int64_t MarkCol = GetMarkColour(); if (MarkCol > 0) { LColour Col((uint32_t)MarkCol, 32); LColour CtxBack(Ctx.Back); LColour Mixed = Col.Mix(CtxBack, MarkColourMix); Ctx.Back = Mixed; } if (Parent) ((ThingList*)Parent)->CurrentMail = this; LListItem::OnPaint(Ctx); if (Parent) ((ThingList*)Parent)->CurrentMail = 0; LFont *PreviewFont = App->GetPreviewFont(); LFont *ColumnFont = Parent && Parent->GetFont() ? Parent->GetFont() : LSysFont; if (Parent && PreviewLines && PreviewFont && ColumnFont && GetObject() && !GetObject()->GetInt(FIELD_DONT_SHOW_PREVIEW) && !(GetFlags() & MAIL_READ)) { int y = ColumnFont->GetHeight() + 2; int Space = (Ctx.Y() - y) >> 1; int Line = MAX(PreviewFont->GetHeight(), Space); PreviewFont->Transparent(true); // Setup if (!PreviewCache[0] || abs(PreviewCacheX-Ctx.X()) > 20) { PreviewCache.DeleteObjects(); PreviewCacheX = Ctx.X(); LVariant v; if (GetValue("BodyAsText[1024]", v) && v.Str()) { char16 *Base = Utf8ToWide(v.Str()); char16 *Txt = Base; int i; for (i=0; Txt[i]; i++) { if (Txt[i] < ' ') { Txt[i] = ' '; } } auto TxtLen = StrlenW(Txt); for (i=0; Txt && *Txt && i<2; i++) { LDisplayString *Temp = new LDisplayString(PreviewFont, Txt, MIN(1024, TxtLen)); if (Temp) { int W = Ctx.X()-18; ssize_t Cx = Temp->CharAt(W); if (Cx > 0 && Cx <= TxtLen) { LDisplayString *d = new LDisplayString(PreviewFont, Txt, Cx); if (d) { PreviewCache.Insert(d); } DeleteObj(Temp); Txt += Cx; // LSeekUtf8 TxtLen -= Cx; } else break; } } DeleteArray(Base); } } // Display LColour PreviewCol(App->GetColour(L_MAIL_PREVIEW)); if (Select()) { int GreyPrev = PreviewCol.GetGray(); int GreyBack = Ctx.Back.GetGray(); int d = GreyPrev - GreyBack; if (d < 0) d = -d; if (d < 128) { // too near PreviewFont->Colour(Ctx.Fore, Ctx.Back); } else { // ok PreviewFont->Colour(PreviewCol, Ctx.Back); } } else { PreviewFont->Colour(PreviewCol, Ctx.Back); } for (auto p: PreviewCache) { p->Draw(Ctx.pDC, Ctx.x1+16, Ctx.y1+y, 0); y += Line; } } } bool Mail::GetFormats(bool Export, LString::Array &MimeTypes) { if (Export) { MimeTypes.Add("text/plain"); MimeTypes.Add(sMimeMbox); } MimeTypes.Add(sMimeMessage); return MimeTypes.Length() > 0; } Thing::IoProgress Mail::Import(IoProgressImplArgs) { if (!mimeType) IoProgressError("No mime type."); if (Stricmp(mimeType, sMimeMessage)) IoProgressNotImpl(); // Single email.. OnAfterReceive(stream); int Flags = GetFlags(); Flags &= ~MAIL_CREATED; Flags |= MAIL_READ; SetFlags(Flags); Update(); IoProgressSuccess(); } #define TIMEOUT_OBJECT_LOAD 20000 Thing::IoProgress Mail::Export(IoProgressImplArgs) { if (!mimeType) IoProgressError("No mimetype."); if (!Stricmp(mimeType, "text/plain")) { LStringPipe Buf; char Str[256]; char Eol[] = EOL_SEQUENCE; // Addressee if (GetFlags() & MAIL_CREATED) { Buf.Push("To:"); for (LDataPropI *a=GetTo()->First(); a; a=GetTo()->Next()) { sprintf_s(Str, sizeof(Str), "\t%s <%s>%s", a->GetStr(FIELD_NAME), a->GetStr(FIELD_EMAIL), Eol); Buf.Push(Str); } } else { sprintf_s(Str, sizeof(Str), "From: %s <%s>%s", GetFrom()->GetStr(FIELD_NAME), GetFrom()->GetStr(FIELD_EMAIL), Eol); Buf.Push(Str); } // Subject sprintf_s(Str, sizeof(Str), "Subject: %s%s", GetSubject(), Eol); Buf.Push(Str); // Sent date if (GetDateSent()->Year()) { int ch = sprintf_s(Str, sizeof(Str), "Sent Date: "); GetDateSent()->Get(Str+ch, sizeof(Str)-ch); } else { int ch = sprintf_s(Str, sizeof(Str), "Date Received: "); GetDateReceived()->Get(Str+ch, sizeof(Str)-ch); } strcat(Str, Eol); Buf.Push(Str); // Body Buf.Push(Eol); Buf.Push(GetBody()); // Write the output auto s = Buf.NewGStr(); if (!s) IoProgressError("No data to output."); stream->Write(s.Get(), s.Length()); IoProgressSuccess(); } else if (!Stricmp(mimeType, sMimeMbox)) { char Temp[256]; // generate from header sprintf_s(Temp, sizeof(Temp), "From %s ", GetFrom()->GetStr(FIELD_EMAIL)); struct tm Ft; ZeroObj(Ft); LDateTime Rec = *GetDateReceived(); if (!Rec.Year()) Rec.SetNow(); Ft.tm_sec = Rec.Seconds(); /* seconds after the minute - [0,59] */ Ft.tm_min = Rec.Minutes(); /* minutes after the hour - [0,59] */ Ft.tm_hour = Rec.Hours(); /* hours since midnight - [0,23] */ Ft.tm_mday = Rec.Day(); /* day of the month - [1,31] */ Ft.tm_mon = Rec.Month() - 1; /* months since January - [0,11] */ Ft.tm_year = Rec.Year() - 1900; /* years since 1900 */ Ft.tm_wday = Rec.DayOfWeek(); strftime(Temp+strlen(Temp), MAX_NAME_SIZE, "%a %b %d %H:%M:%S %Y", &Ft); strcat(Temp, "\r\n"); // write mail stream->Write(Temp, strlen(Temp)); auto Status = Export(stream, sMimeMessage, [cb](auto io, auto stream) { if (io->status == Store3Success) stream->Write((char*)"\r\n.\r\n", 2); else if (io->status == Store3Delayed) LAssert(!"We should never get delayed here... it's the callback!"); if (cb) cb(io, stream); }); return Status; } else if (!Stricmp(mimeType, sMimeMessage)) { // This function can't be asynchronous, it must complete with UI or waiting for a callback. // Because it is used by the drag and drop system. Which won't wait. auto state = GetLoaded(); if (state != Store3Loaded) IoProgressError("Mail not loaded."); if (!GetObject()) IoProgressError("No store object."); auto Data = GetObject()->GetStream(_FL); if (!Data) IoProgressError("Mail for export has no data."); Data->SetPos(0); LCopyStreamer Cp(512<<10); if (Cp.Copy(Data, stream) <= 0) IoProgressError("Mail copy stream failed."); IoProgressSuccess(); } IoProgressNotImpl(); } char *Mail::GetNewText(int Max, const char *AsCp) { LAutoString CharSet = GetCharSet(); char *Txt = 0; // This limits the preview of the text to // the first 64kb, which is reasonable. int Len = Max > 0 ? Strnlen(GetBody(), Max) : -1; // Convert or allocate the text. if (CharSet) { Txt = (char*)LNewConvertCp(AsCp, GetBody(), GetBodyCharset(), Len); } else { Txt = NewStr(GetBody(), Len); } return Txt; } LAutoString Mail::GetCharSet() { LAutoString Status; LAutoString ContentType; if (GetBodyCharset()) { // If the charset has a stray colon... char *Colon = strchr(GetBodyCharset(), ';'); // Kill it. if (Colon) *Colon = 0; // Copy the string.. Status.Reset(NewStr(GetBodyCharset())); } if (!GetBodyCharset()) { ContentType.Reset(InetGetHeaderField(GetInternetHeader(), "Content-Type")); if (ContentType) { char *CharSet = stristr(ContentType, "charset="); if (CharSet) { CharSet += 8; if (*CharSet) { if (strchr("\"\'", *CharSet)) { char Delim = *CharSet++; char *e = CharSet; while (*e && *e != Delim) { e++; } Status.Reset(NewStr(CharSet, e - CharSet)); } else { char *e = CharSet; while (*e && (IsDigit(*e) || IsAlpha(*e) || strchr("-_", *e))) { e++; } Status.Reset(NewStr(CharSet, e - CharSet)); } } } } /* // If it's not a valid charset... if (!LGetCsInfo(Status)) { // Then kill it. Status.Reset(); if (GetBodyCharset()) { SetBodyCharset(0); SetDirty(); } } */ } return Status; } /* Old Body Printing Code int Lines = 0; char *n; for (n=Body.Str(); n && *n; n++) { if (*n == '\n') Lines++; } char *c = Body.Str(); if (c) { c = WrapLines(c, c ? strlen(c) : 0, 76); Lines = 0; for (n=c; *n; n++) { if (*n == '\n') Lines++; } while (c && *c) { if (Cy + Line > r.y2) { pDC->EndPage(); if (Window->GetMaxPages() > 0 && ++Page >= Window->GetMaxPages()) { break; } pDC->StartPage(); Cy = 0; } char *Eol = strchr(c, '\n'); int Len = 0; int Ret = 0; if (Eol) { Len = (int) Eol - (int) c; if (Len > 0 && c[Len-1] == '\r') { Len--; Ret = 1; } } else { Len = strlen(c); } MailFont->Text(pDC, r.x1, Cy, c, Len); Cy += Line; c += Len + Ret; if (*c == '\n') c++; } } */ int Mail::OnNotify(LViewI *Ctrl, LNotification n) { switch (Ctrl->GetId()) { case IDC_TEXT: { LDocView *Doc = dynamic_cast(Ctrl); if (Doc) { if (n.Type == LNotifyShowImagesChanged) { if (Doc->GetLoadImages() ^ TestFlag(GetFlags(), MAIL_SHOW_IMAGES)) { if (Doc->GetLoadImages()) { SetFlags(GetFlags() | MAIL_SHOW_IMAGES); } else { SetFlags(GetFlags() & ~MAIL_SHOW_IMAGES); } } } if (n.Type == LNotifyFixedWidthChanged) { bool DocFixed = Doc->GetFixedWidthFont(); bool MailFixed = TestFlag(GetFlags(), MAIL_FIXED_WIDTH_FONT); if (DocFixed ^ MailFixed) { if (Doc->GetFixedWidthFont()) { SetFlags(GetFlags() | MAIL_FIXED_WIDTH_FONT); } else { SetFlags(GetFlags() & ~MAIL_FIXED_WIDTH_FONT); } } } if (n.Type == LNotifyCharsetChanged && dynamic_cast(Doc)) { char s[256]; sprintf_s(s, sizeof(s), ">%s", Doc->GetCharset()); SetHtmlCharset(s); } } break; } } return 0; } void Mail::OnPrintHeaders(ScribePrintContext &Context) { char Str[256]; // print document LDrawListSurface *Page = Context.Pages.Last(); int Line = Context.AppFont->GetHeight(); Page->SetFont(Context.AppFont); LDisplayString *Ds = Context.Text(LLoadString(IDS_MAIL_MESSAGE)); Context.CurrentY += Line; LDataPropI *Ad = GetTo()->First(); if (Ad) { sprintf_s(Str, sizeof(Str), "%s: ", LLoadString(FIELD_TO)); Ds = Page->Text(Context.MarginPx.x1, Context.CurrentY, Str); int x = Ds->X(); for (; Ad; Ad = GetTo()->Next()) { if (Ad->GetStr(FIELD_EMAIL) && Ad->GetStr(FIELD_NAME)) { sprintf_s(Str, sizeof(Str), "%s <%s>", Ad->GetStr(FIELD_NAME), Ad->GetStr(FIELD_EMAIL)); } else if (Ad->GetStr(FIELD_EMAIL)) { strcpy_s(Str, sizeof(Str), Ad->GetStr(FIELD_EMAIL)); } else if (Ad->GetStr(FIELD_EMAIL)) { strcpy_s(Str, sizeof(Str), Ad->GetStr(FIELD_EMAIL)); } else continue; Ds = Page->Text(Context.MarginPx.x1 + x, Context.CurrentY, Str); Context.CurrentY += Ds->Y(); } } if (GetFrom()->GetStr(FIELD_EMAIL) || GetFrom()->GetStr(FIELD_NAME)) { sprintf_s(Str, sizeof(Str), "%s: ", LLoadString(FIELD_FROM)); Ds = Page->Text(Context.MarginPx.x1, Context.CurrentY, Str); int x = Ds->X(); if (GetFrom()->GetStr(FIELD_EMAIL) && GetFrom()->GetStr(FIELD_NAME)) { sprintf_s(Str, sizeof(Str), "%s <%s>", GetFrom()->GetStr(FIELD_NAME), GetFrom()->GetStr(FIELD_EMAIL)); } else if (GetFrom()->GetStr(FIELD_EMAIL)) { strcpy_s(Str, sizeof(Str), GetFrom()->GetStr(FIELD_EMAIL)); } else if (GetFrom()->GetStr(FIELD_NAME)) { strcpy_s(Str, sizeof(Str), GetFrom()->GetStr(FIELD_NAME)); } else Str[0] = 0; Ds = Page->Text(Context.MarginPx.x1 + x, Context.CurrentY, Str); Context.CurrentY += Ds->Y(); } { // Subject LString s; const char *Subj = GetSubject(); s.Printf("%s: %s", LLoadString(FIELD_SUBJECT), Subj?Subj:""); Context.Text(s); } { // Date LString s; GetDateSent()->Get(Str, sizeof(Str)); s.Printf("%s: %s", LLoadString(IDS_DATE), Str); Context.Text(s); } // attachment list... List Attached; if (GetAttachments(&Attached) && Attached.Length() > 0) { sprintf_s(Str, sizeof(Str), "%s: ", LLoadString(IDS_ATTACHMENTS)); Ds = Page->Text(Context.MarginPx.x1, Context.CurrentY, Str); int x = Ds->X(); for (auto a: Attached) { char Size[32]; LFormatSize(Size, sizeof(Size), a->GetSize()); sprintf_s(Str, sizeof(Str), "%s (%s)", a->GetName(), Size); Ds = Page->Text(Context.MarginPx.x1 + x, Context.CurrentY, Str); Context.CurrentY += Ds->Y(); } } // separator line LRect Sep(Context.MarginPx.x1, Context.CurrentY + (Line * 5 / 10), Context.MarginPx.x2, Context.CurrentY + (Line * 6 / 10)); Page->Rectangle(&Sep); Context.CurrentY += Line; } void Mail::OnPrintText(ScribePrintContext &Context, LPrintPageRanges &Pages) { // Text printing LDrawListSurface *Page = Context.Pages.Last(); LVariant Body; if (!GetVariant("BodyAsText", Body) || !Body.Str()) { LgiTrace("%s:%i - No content to print.\n", _FL); return; } LAutoWString w(Utf8ToWide(Body.Str())); if (!w) { LgiTrace("%s:%i - Utf8ToWide failed.\n", _FL); return; } Page->SetFont(Context.MailFont); for (char16 *s = w; s && *s; ) { // Find end of line.. char16 *n = StrchrW(s, '\n'); if (!n) n = s + StrlenW(s); ssize_t LineLen = n - s; // Find how many characters will fit on the page ssize_t Fit = 0; if (n > s) { LDisplayString a(Context.MailFont, s, MIN(LineLen, 1024)); Fit = a.CharAt(Context.MarginPx.X()); } if (Fit < 0) { break; } char16 *e = s + Fit; if (e < n) { // The whole line doesn't fit on the page... // Find the best breaking opportunity before that... #define ExtraBreak(c) ( ( (c) >= 0x3040 && (c) <= 0x30FF ) || \ ( (c) >= 0x3300 && (c) <= 0x9FAF ) \ ) char16 *StartE = e; while (e > s) { if (e[-1] == ' ' || ExtraBreak(e[-1])) { break; } e--; } if (e == s) { e = StartE; } } // Output the segment of text bool HasRet = e > s ? e[-1] == '\r' : false; LString Str(s, (e - s) - (HasRet ? 1 : 0)); Context.Text(Str); // Update the pointers s = e; if (*s == '\n') s++; } } /// \returns the number of pages printed int Mail::OnPrintHtml(ScribePrintContext &Context, LPrintPageRanges &Pages, LSurface *RenderedHtml) { // HTML printing... LDrawListSurface *Page = Context.Pages.Last(); // Work out the scaling from memory bitmap to page.. double MemScale = (double) Context.pDC->X() / (double) RenderedHtml->X(); // Now paint the bitmap onto the existing page int PageIdx = 0, Printed = 0; for (int y = 0; y < RenderedHtml->Y(); PageIdx++) { if (Pages.InRanges(PageIdx)) { // Work out how much bitmap we can paint onto the current page... int PageRemaining = Context.MarginPx.Y() - Context.CurrentY; int MemPaint = (int) (PageRemaining / MemScale); // This is how much of the memory context we can fit on the page LRect MemRect(0, y, RenderedHtml->X()-1, y + MemPaint - 1); LRect Bnds = RenderedHtml->Bounds(); MemRect.Bound(&Bnds); // Work out how much page that is take up int PageHeight = (int) (MemRect.Y() * MemScale); // This is the position on the page we are blting to LRect PageRect(Context.MarginPx.x1, Context.CurrentY, Context.MarginPx.x2, Context.CurrentY + PageHeight - 1); // Do the blt Page->StretchBlt(&PageRect, RenderedHtml, &MemRect); Printed++; // Now move our position down the page.. Context.CurrentY += PageHeight; if ((Context.MarginPx.Y() - Context.CurrentY) * 100 / Context.MarginPx.Y() < 5) { // Ok we hit the end of the page and need to go to the next page Context.CurrentY = Context.MarginPx.y1; Page = Context.NewPage(); } y += MemRect.Y(); } } return Printed; } ////////////////////////////////////////////////////////////////////////////// size_t Mail::Length() { size_t Status = 0; for (auto a: Attachments) { if (a->GetMimeType() && _stricmp(a->GetMimeType(), sMimeMessage) == 0) { Status++; } } return Status; } ssize_t Mail::IndexOf(Mail *m) { ssize_t Status = -1; ssize_t n = 0; for (auto a: Attachments) { if (a->GetMimeType() && _stricmp(a->GetMimeType(), sMimeMessage) == 0) { if (a->GetMsg() == m) { Status = n; break; } n++; } } return Status; } Mail *Mail::operator [](size_t i) { size_t n = 0; for (auto a: Attachments) { if (a->GetMimeType() && _stricmp(a->GetMimeType(), sMimeMessage) == 0) { if (n == i) return a->GetMsg(); n++; } } return NULL; } bool Mail::LoadFromFile(char *File) { if (File) { LAutoPtr f(new LFile); if (f->Open(File, O_READ)) { return Import(AutoCast(f), sMimeMessage); } } return false; } /////////////////////////////////////////////////////////////////////////////////////////////////////// bool CreateMailAddress(LStream &Out, LDataPropI *Addr, MailProtocol *Protocol) { if (!Addr) return false; auto Name = Addr->GetStr(FIELD_NAME); auto Email = Addr->GetStr(FIELD_EMAIL); if (!Email) return false; Name = EncodeRfc2047(NewStr(Name), 0, &Protocol->CharsetPrefs); if (Name) { if (strchr(Name, '\"')) Out.Print("'%s' ", Name); else Out.Print("\"%s\" ", Name); DeleteArray(Name); } Out.Print("<%s>", Email); return true; } bool CreateMailHeaders(ScribeWnd *App, LStream &Out, LDataI *Mail, MailProtocol *Protocol) { bool Status = true; // Setup char Buffer[1025]; // Construct date LDateTime Dt; int TimeZone = Dt.SystemTimeZone(); Dt.SetNow(); sprintf_s(Buffer, sizeof(Buffer), "Date: %s, %i %s %i %i:%2.2i:%2.2i %s%2.2d%2.2d\r\n", LDateTime::WeekdaysShort[Dt.DayOfWeek()], Dt.Day(), LDateTime::MonthsShort[Dt.Month()-1], Dt.Year(), Dt.Hours(), Dt.Minutes(), Dt.Seconds(), (TimeZone >= 0) ? "+" : "", TimeZone / 60, abs(TimeZone) % 60); Status &= Out.Write(Buffer, strlen(Buffer)) > 0; if (Protocol && Protocol->ProgramName) { // X-Mailer: Status &= Out.Print("X-Mailer: %s\r\n", Protocol->ProgramName.Get()) > 0; } if (Protocol && Protocol->ExtraOutgoingHeaders) { for (char *s=Protocol->ExtraOutgoingHeaders; s && *s; ) { char *e = s; while (*e && *e != '\r' && *e != '\n') e++; ssize_t l = e-s; if (l > 0) { Status &= Out.Write(s, l) > 0; Status &= Out.Write((char*)"\r\n", 2) > 0; } while (*e && (*e == '\r' || *e == '\n')) e++; s = e; } } int Priority = (int)Mail->GetInt(FIELD_PRIORITY); if (Priority != MAIL_PRIORITY_NORMAL) { // X-Priority: Status &= Out.Print("X-Priority: %i\r\n", Priority) > 0; } uint32_t MarkColour = (uint32_t)Mail->GetInt(FIELD_COLOUR); if (MarkColour) { // X-Color (HTML Colour Ref for email marking) Status &= Out.Print("X-Color: #%2.2X%2.2X%2.2X\r\n", R24(MarkColour), G24(MarkColour), B24(MarkColour)) > 0; } // Message-ID: auto MessageID = Mail->GetStr(FIELD_MESSAGE_ID); if (MessageID) { for (auto m=MessageID; *m; m++) { if (*m <= ' ') { printf("%s:%i - Bad message ID '%s'\n", _FL, MessageID); return false; } } Status &= Out.Print("Message-ID: %s\r\n", MessageID) > 0; } // References: auto References = Mail->GetStr(FIELD_REFERENCES); if (ValidStr(References)) { auto Dir = strrchr(References, '/'); LString Ref; if (Dir) { LUri u; Ref = u.DecodeStr(Dir + 1); } else Ref = References; if (*Ref == '<') Status &= Out.Print("References: %s\r\n", Ref.Get()) > 0; else Status &= Out.Print("References: <%s>\r\n", Ref.Get()) > 0; } // To: - GDataIt Addr = Mail->GetList(FIELD_TO); + LDataIt Addr = Mail->GetList(FIELD_TO); LArray Objs; LArray To, Cc; ContactGroup *Group; for (unsigned i=0; iLength(); i++) { LDataPropI *a = (*Addr)[i]; EmailAddressType AddrType = (EmailAddressType)a->GetInt(FIELD_CC); LString Addr = a->GetStr(FIELD_EMAIL); if (LIsValidEmail(Addr)) { if (AddrType == MAIL_ADDR_CC) Cc.Add(a); else if (AddrType == MAIL_ADDR_TO) To.Add(a); } else if ((Group = LookupContactGroup(App, Addr))) { Group->UsedTs.SetNow(); LString::Array Addrs = Group->GetAddresses(); for (unsigned n=0; nGetObject()->GetStore(), NULL); if (sa) { sa->Addr = Addrs[n]; Objs.Add(sa); if (AddrType == MAIL_ADDR_CC) Cc.Add(sa); else if (AddrType == MAIL_ADDR_TO) To.Add(sa); } } } } if (To.Length()) { for (unsigned i=0; iGetObj(FIELD_FROM); if (From && From->GetStr(FIELD_EMAIL)) { Out.Print("From: "); if (!CreateMailAddress(Out, From, Protocol)) return false; Out.Print("\r\n"); } else { LgiTrace("%s:%i - No from address.\n", _FL); return false; } // Reply-To: LDataPropI *Reply = Mail->GetObj(FIELD_REPLY); if (Reply && ValidStr(Reply->GetStr(FIELD_EMAIL))) { Out.Print("Reply-To: "); if (!CreateMailAddress(Out, Reply, Protocol)) return false; Out.Print("\r\n"); } // Subject: char *Subj = EncodeRfc2047(NewStr(Mail->GetStr(FIELD_SUBJECT)), 0, &Protocol->CharsetPrefs, 9); sprintf_s(Buffer, sizeof(Buffer), "Subject: %s\r\n", (Subj) ? Subj : ""); Status &= Out.Write(Buffer, strlen(Buffer)) > 0; DeleteArray(Subj); // DispositionNotificationTo uint8_t DispositionNotificationTo = TestFlag(Mail->GetInt(FIELD_FLAGS), MAIL_READ_RECEIPT); if (DispositionNotificationTo && From) { int ch = sprintf_s(Buffer, sizeof(Buffer), "Disposition-Notification-To:"); char *Nme = EncodeRfc2047(NewStr(From->GetStr(FIELD_NAME)), 0, &Protocol->CharsetPrefs); if (Nme) { ch += sprintf_s(Buffer+ch, sizeof(Buffer)-ch, " \"%s\"", Nme); DeleteArray(Nme); } ch += sprintf_s(Buffer+ch, sizeof(Buffer)-ch, " <%s>\r\n", From->GetStr(FIELD_EMAIL)); Status &= Out.Write(Buffer, ch) > 0; } // Content-Type LDataPropI *Root = Mail->GetObj(FIELD_MIME_SEG); if (Root) { auto MimeType = Root->GetStr(FIELD_MIME_TYPE); auto Charset = Root->GetStr(FIELD_CHARSET); if (MimeType) { LString s; s.Printf("Content-Type: %s%s%s\r\n", MimeType, Charset?"; charset=":"", Charset?Charset:""); Status &= Out.Write(s, s.Length()) == s.Length(); } } else LAssert(0); Objs.DeleteObjects(); return Status; } diff --git a/Code/ScribePrivate.h b/Code/ScribePrivate.h --- a/Code/ScribePrivate.h +++ b/Code/ScribePrivate.h @@ -1,779 +1,780 @@ /*hdr ** FILE: ScribePrivate.h ** AUTHOR: Matthew Allen ** DATE: 22/10/97 ** DESCRIPTION: Scribe email application ** ** Copyright (C) 1998-2002 Matthew Allen ** fret@memecode.com */ // Includes #ifndef __SCRIBE_PRIVATE__ #define __SCRIBE_PRIVATE__ #include "lgi/common/XmlTreeUi.h" #include "lgi/common/Html.h" #include "lgi/common/CheckBox.h" #include "lgi/common/TabView.h" #include "lgi/common/RadioGroup.h" // Defines #define SCRIBE_TOOLBAR_BORDER_SPACING_PX 3 // Externs extern const char *AppName; extern char HelpFile[]; extern const char *DefaultFolderNames[]; extern const char *DefaultRfXml; extern int DefaultFilterFields[]; extern char MailToStr[]; extern char SubjectStr[]; extern char ContentTypeDefault[]; extern bool OptionSizeInKiB; extern bool ShowRelativeDates; // Mime types extern char sMimeVCard[]; extern char sMimeVCalendar[]; extern char sMimeICalendar[]; extern char sMimeMbox[]; extern char sMimeLgiResource[]; extern char sMimeMessage[]; extern char sMimeXml[]; // Default templates extern char DefaultTextReplyTemplate[]; extern char DefaultHtmlReplyTemplate[]; //////////////////////////////////////////////////////////////////////////////////////////// // Functions // Misc extern int SizeOfFile(char *FileName); extern char *OsName(); extern LString GetFullAppName(bool Platform = true); extern void LogMsg(char *str, ...); // Dnd and clipboard format for an array of "Thing*" extern char ScribeThingList[]; #define ScribeThingMagic "thng" #define ScribeFolderMagic "fldr" class ScribeClipboardFmt { char Magic[4]; OsProcessId ProcessId; uint32_t Len; union { Thing *Things[1]; // 'Len' long... ScribeFolder *Folders[1]; }; static bool Is(const char *Type, void *Ptr, size_t Len); public: static ScribeClipboardFmt *Alloc(bool ForFolders, size_t Size); static ScribeClipboardFmt *Alloc(List &Lst); static ScribeClipboardFmt *Alloc(LArray &Arr); static bool IsThing(void *Ptr, size_t Bytes) { return Is(ScribeThingMagic, Ptr, Bytes); } static bool IsFolder(void *Ptr, size_t Bytes) { return Is(ScribeFolderMagic, Ptr, Bytes); } size_t Sizeof(); uint32_t Length() { return Len; } Thing *ThingAt(size_t Idx, Thing *Set = NULL); ScribeFolder *FolderAt(size_t Idx, ScribeFolder *Set = NULL); }; extern LToolBar *LoadToolbar(LView *Parent, const char *File); extern LString ScribeGetFileMimeType(const char *File); // extern void UpgradeRfOption(ObjProperties *Opts, const char *New, const char *Old, const char *Default); extern ScribeFolder *CastFolder(LDataI *f); extern int MakeOpenFlags(ScribeAccount *a, bool Send); extern bool HasEmoji(char *Txt); extern bool HasEmoji(uint32_t *Txt); extern LAutoWString TextToEmoji(uint32_t *Txt, bool IsHtml); extern int FilterCompare(Filter *a, Filter *b, NativeInt Data); extern bool ExtractHtmlContent( LString &OutHtml, LString &Charset, LString &Styles, const char *InHtml); // Scripting extern bool OnFilterScript(Filter *f, Mail *m, const char *script); extern bool OnToolScript(ScribeWnd *App, const char *File); //////////////////////////////////////////////////////////////////////////////////////////// // Classes class MailTree; class ScribeWnd; class Mail; class Contact; class MailUi; class ScribeFolder; class ContactUi; class FolderPropertiesDlg; class ScribeAccount; class Filter; class Attachment; class Calendar; class CalendarSource; class ScribeApp : public LApp { public: int Status; ScribeApp(OsAppArguments &AppArgs, LAppArguments *Opts); }; //////////////////////////////////////////////////////////////////////// extern ItemFieldDef MailFieldDefs[]; extern ItemFieldDef ContactFieldDefs[]; extern ItemFieldDef CalendarFields[]; extern ItemFieldDef FilterFieldDefs[]; extern ItemFieldDef *GetFieldDefByName(char *Name); extern ItemFieldDef *GetFieldDefById(int Id); //////////////////////////////////////////////////////////////////////// // extern void StorageCount(StorageItem *Store, Counter &c); class ContactUi : public ThingUi, public LResourceLoad { protected: Contact *Item; LList *Lst; class LContactImage *ImgView; bool InitField(int Id, const char *Name); bool SaveField(int Id, const char *Name); public: ContactUi(Contact *item); ~ContactUi(); void OnLoad(); void OnSave(); void OnDestroy(); int OnNotify(LViewI *Col, LNotification n); LMessage::Result OnEvent(LMessage *Msg); void OnPosChange(); void OnPulse(); }; ///////////////////////////////////////////////////////////// #define DefaultMarkColour (MarkColours32[5]) #define IDM_MARK_MAX 8 #define IDM_UNMARK 30000 #define IDM_MARK_ALL 30001 #define IDM_SELECT_NONE 30002 #define IDM_SELECT_ALL 30003 #define IDM_MARK_BASE 30100 #define IDM_MARK_SELECT_BASE 30200 #define IDM_IDENTITY_BASE 30300 enum MarkedState { MS_None, MS_One, MS_Multiple }; extern uint32_t MarkColours32[IDM_MARK_MAX]; extern LSubMenu *BuildMarkMenu( LSubMenu *MarkMenu, MarkedState MarkState, uint32_t SelectedMark, bool None = false, bool All = false, bool Select = false); class AddressList : public LList, public LDragDropTarget { ScribeWnd *App; public: AddressList(ScribeWnd *app, int id, int x, int y, int cx, int cy, const char *name = "List"); const char *GetClass() { return "AddressList"; } void OnCreate(); bool OnKey(LKey &k); - void OnInit(GDataIt l); - void OnSave(LDataStoreI *store, GDataIt l); + void OnInit(LDataIt l); + void OnSave(LDataStoreI *store, LDataIt l); void OnItemClick(LListItem *Item, LMouse &m); void Copy(); void Paste(); // D'n'd support int WillAccept(LDragFormats &Formats, LPoint Pt, int KeyState); int OnDrop(LArray &Data, LPoint Pt, int KeyState); }; struct BrowseItem { int Score; LString First; LString Last; LString Email; BrowseItem(const char *first, const char *last, const char *email, int score) { Score = score; First = first; Last = last; Email = email; } }; class MailUi : public ThingUi, public MailContainerIter, public MailViewOwner { friend class AddressList; friend class Mail; friend class ScribeTextPipe; LViewI *WorkingDlg; protected: int AddMode; int Sx, Sy; int CmdAfterResize; bool MetaFieldsDirty; bool HtmlCtrlDirty; bool TextCtrlDirty; bool IgnoreShowImgNotify; int CurrentEditCtrl; MissingCapsBar *MissingCaps; LCapabilityTarget::CapsHash Caps; // Commands LScriptUi Commands; LToolButton *BtnPrev; LToolButton *BtnNext; LToolButton *BtnSend; LToolButton *BtnSave; LToolButton *BtnSaveClose; LToolButton *BtnAttach; LToolButton *BtnReply; LToolButton *BtnReplyAll; LToolButton *BtnForward; LToolButton *BtnBounce; // Gpg class MailUiGpg *GpgUi; // To: LPanel *ToPanel; LEdit *Entry; class AddressBrowse *Browse; LCombo *SetTo; AddressList *To; LCombo *Remove; // From LPanel *FromPanel; AddressList *FromList; LCombo *FromCbo; LArray FromAccountId; // GDropDown *Drop; LPanel *ReplyToPanel; LCheckBox *ReplyToChk; LCombo *ReplyToCbo; // Subject LPanel *SubjectPanel; LEdit *Subject; // Calendar panel LPanel *CalendarPanel; LView *CalPanelStatus; // Tabs LTabView *Tab; LTabPage *TabText; bool TextLoaded; LDocView *TextView; LTabPage *TabHtml; bool HtmlLoaded; LDocView *HtmlView; LTabPage *TabAttachments; class AttachmentList *Attachments; LTabPage *TabHeader; LDocView *Header; // Methods bool SeekMsg(int Delta); bool NeedsCapability(const char *Name, const char *Param = NULL); LDocView *GetDoc(const char *MimeType); bool SetDoc(LDocView *v, const char *MimeType); void OnInstall(LCapabilityTarget::CapsHash *Caps, bool Status); void OnCloseInstaller(); void SetCmdAfterResize(int Cmd); void OnChildrenChanged(LViewI *Wnd, bool Attaching); public: MailUi(Mail *item, MailContainer *Container = 0); ~MailUi(); const char *GetClass() { return "MailUi"; } Mail *GetItem(); void SetItem(Mail *m); bool SetDirty(bool d, bool ui = true); void OnLoad(); AttachmentList *GetAttachments() { return Attachments; } void SerializeText(bool FromCtrl); bool AddRecipient(Contact *c); bool AddRecipient(const char *Email, const char *Name); bool AddRecipient(AddressDescriptor *Addr); bool AddCalendarEvent(bool AddPopupReminder); int OnNotify(LViewI *Col, LNotification n); void OnSysKey(int a, int b); void OnDirty(bool Dirty); void OnDataEntered(); void OnSave(); void OnPaint(LSurface *pDC); void OnPulse(); LMessage::Result OnEvent(LMessage *Msg); void OnPosChange(); int OnCommand(int Cmd, int Event, OsView Window); int HandleCmd(int Cmd); void OnReceiveFiles(LArray &Files); bool OnViewKey(LView *v, LKey &k); void OnAttachmentsChange(); void OnChange(); bool IsWorking(int Set = -1); bool OnRequestClose(bool OsClose); bool CallMethod(const char *Name, LVariant *Dst, LArray &Arg); }; class AttachmentList : public LList { MailUi *Ui; public: AttachmentList(int id, int x, int y, int cx, int cy, MailUi *ui); ~AttachmentList(); void OnItemClick(LListItem *Item, LMouse &m); bool OnKey(LKey &k); }; // this is the list pane that displays the // contents of a ScribeFolder class ThingList : public LList { friend class Mail; ScribeFolder *Container; Mail *CurrentMail; int BoldUnread; ScribeWnd *App; public: ThingList(ScribeWnd *wnd); ~ThingList(); const char *GetClass() { return "ThingList"; } LRect &GetClient(bool ClientSpace = true); int GetSortCol() { return (Container) ? Container->GetSortCol() : 0; } int GetSortField() { return (Container) ? Container->GetSortField() : 0; } bool GetSortAscending() { return (Container) ? Container->GetSortAscend() != 0 : 0; } void SetSort(int Col, int Ascend); void ReSort(); ScribeFolder *GetContainer() { return Container; } void SetContainer(ScribeFolder *c) { Container = c; } LFont *GetFont(); void OnPaint(LSurface *pDC); void OnColumnClick(int Col, LMouse &m); void OnItemClick(LListItem *Item, LMouse &m); void OnItemSelect(LArray &Item); bool OnKey(LKey &k); void OnColumnDrag(int Col, LMouse &m); bool OnColumnReindex(LItemColumn *Col, int OldIndex, int NewIndex); List PlaceHolders; void DeletePlaceHolders(); }; // this is the tree view on the left hand side // it contains all the ThingContainers class MailTree : public LTree, public LDragDropTarget { protected: ScribeWnd *App; LTreeItem *LastHit; int8 LastWasRoot; ThingList *Things() { return App->GetMailList(); } void OnCreate(); int WillAccept(LDragFormats &Formats, LPoint Pt, int KeyState); int OnDrop(LArray &Data, LPoint Pt, int KeyState); void OnDragEnter() { LTree::OnDragEnter(); } void OnDragExit() { LTree::OnDragExit(); } public: MailTree(ScribeWnd *app); ~MailTree(); const char *GetClass() { return "MailTree"; } ssize_t Sizeof(); bool Serialize(LFile &f, bool Write); void OnCreateSubDirectory(ScribeFolder *Item); void OnDelete(ScribeFolder *Item, bool Force); void OnProperties(ScribeFolder *Item); void OnItemClick(LTreeItem *Item, LMouse &m); void OnItemSelect(LTreeItem *Item); LMessage::Result OnEvent(LMessage *Msg); }; ////////////////////////////////////////////////////////////// class FilterUi : public ThingUi { protected: struct FilterUiPriv *d; Filter *Item; LDocView *Script; LScriptUi Commands; LTabView *Tab; class LFilterView *Conditions; LList *Actions; void OnLoad(); void OnSave(); bool OnViewKey(LView *v, LKey &k); public: FilterUi(Filter *item); ~FilterUi(); // Data access Filter *GetItem() { return Item; } // Events int OnNotify(LViewI *Col, LNotification n); LMessage::Result OnEvent(LMessage *Msg); int OnCommand(int Cmd, int Event, OsView Window); }; /////////////////////////////////////////////////////////////////////////////// extern void LogStrToFile(char *File, char *Str, ...); class ILog { protected: LVariant LogFileName; int LogFmt; LStreamI *Log; bool DeleteLog; Progress *Info; char *EnumFileName(char *File); void WriteLog(const char *s, ssize_t l); public: ILog(LStreamI *log); ~ILog(); void LogRead(char *Data, ssize_t Len); void LogWrite(const char *Data, ssize_t Len); void LogError(int ErrorCode, const char *ErrorDescription); void LogInfomation(const char *Str); bool LogSetVariant(const char *Name, LVariant &v, const char *Array = NULL); }; class ILogConnection : public ILog, public LSocket { public: ILogConnection(LStreamI *log) : ILog(log) { } ~ILogConnection() { } void OnRead(char *Data, ssize_t Len) override { LogRead(Data, Len); } void OnWrite(const char *Data, ssize_t Len) override { LogWrite(Data, Len); } void OnError(int ErrorCode, const char *ErrorDescription) override { LogError(ErrorCode, ErrorDescription); } void OnInformation(const char *Str) override { LogInfomation(Str); } bool SetVariant(const char *Name, LVariant &v, const char *Array = NULL) override { return LogSetVariant(Name, v, Array); } }; class ILogSocks5Connection : public ILog, public LSocks5Socket { public: ILogSocks5Connection( char *proxy, int port, char *username, char *password, LStreamI *log) : ILog(log) { SetProxy(proxy, port, username, password); } void OnRead(char *Data, ssize_t Len) override { if (Socks5Connected) LogRead(Data, Len); } void OnWrite(const char *Data, ssize_t Len) override { if (Socks5Connected) LogWrite(Data, Len); } void OnError(int ErrorCode, const char *ErrorDescription) override { LogError(ErrorCode, ErrorDescription); } void OnInformation(const char *Str) override { LogInfomation(Str); } bool SetVariant(const char *Name, LVariant &v, const char *Array = NULL) override { return LogSetVariant(Name, v, Array); } }; /////////////////////////////////////////////////////////////////////////////// #include "ScribeFolderSelect.h" #include "NewMailDlg.h" #include "SearchView.h" ////////////////////////////////////////////////////////////////////// class ScribePanel : public LPanel { protected: ScribeWnd *App; public: ScribePanel(ScribeWnd *app, const char *name, int size, bool open = true); bool Pour(LRegion &r); }; ////////////////////////////////////////////////////////////////////// // UI class CreateSubFolderDlg : public LDialog { LEdit *FolderName = NULL; LRadioGroup *FolderType = NULL; public: int SubType = -1; LString SubName; CreateSubFolderDlg(LView *parent, int defaulttype = 0, bool *enable = 0, char *default_name = 0); int OnNotify(LViewI *Ctrl, LNotification n); }; class LanguageDlg : public LDialog { class LanguageDlgPrivate *d = NULL; public: bool Ok; LAutoString Lang; LanguageDlg(ScribeWnd *app); ~LanguageDlg(); int OnNotify(LViewI *c, LNotification n); }; class FolderNameDlg : public LDialog { LEdit *Ed; public: char *Name; FolderNameDlg(LView *parent, const char *Old = ""); ~FolderNameDlg(); void OnCreate(); int OnNotify(LViewI *Ctrl, LNotification n); }; //////////////////////////////////////////////////////////////////////////////////////////////// class ScribeAccountItem; class OptionsDlg : public TabDialog, public LXmlTreeUi { friend class ScribeAccountItem; friend class AccountItem; class OptionsDlgPrivate *d; ScribeWnd *App; LFontType EditorFont; LFontType HtmlFont; List Langs; LCombo *UiLang; int SinkHnd; ScribeAccountItem *LastRecord; void UpdateDefaultSendAccounts(); void UpdateFontDescription(); bool PasswordCtrlValue(int CtrlId, char *Option, bool ToWindow); void WriteNativeText(LFile &f, char *t); public: OptionsDlg(ScribeWnd *window); ~OptionsDlg(); void OnCreate(); void OnAccountEnable(ScribeAccount *Acc, bool Enable); int OnNotify(LViewI *Ctrl, LNotification n); LMessage::Result OnEvent(LMessage *m); }; class ScribeThread : public LThread { ScribeWnd *App; int Index; public: ScribeThread(ScribeWnd *app, int index); int Main(); }; // Security Dialog class SecurityDlg : public LDialog, public LXmlTreeUi { class SecurityDlgPrivate *d; public: SecurityDlg(ScribeWnd *app); ~SecurityDlg(); int OnNotify(LViewI *c, LNotification n); }; // Bayesian filtering setup class BayesDlg : public LDialog, public LXmlTreeUi { class BayesDlgPrivate *d; public: BayesDlg(ScribeWnd *app); ~BayesDlg(); int OnNotify(LViewI *c, LNotification n); }; //////////////////////////////////////////////////////////////////////////////////// class DynamicHtml : public Html1::LHtml, public LDefaultDocumentEnv { class DynamicHtmlPrivate *d; public: DynamicHtml(ScribeWnd *app, const char *file); ~DynamicHtml(); char *OnDynamicContent(LDocView *Parent, const char *Code); bool OnNavigate(LDocView *Parent, const char *Uri); }; //////////////////////////////////////////////////////////////////////////////////// class ScribeBehaviour { public: static ScribeBehaviour *New(ScribeWnd *app); virtual ~ScribeBehaviour() {} // Events/Actions virtual bool DoWizard() { return false; } virtual bool SerializeOptions ( LOptionsFile *Opts, char *OptsFile, bool Write ) { return false; } }; //////////////////////////////////////////////////////////////////////////////////// struct SystemFolderInfo { int Id; const char *PathOption; const char *HasOption; }; extern SystemFolderInfo SystemFolders[]; //////////////////////////////////////////////////////////////////////////////////// // Shared memory record format, used to pass argument data to the right running // instance of Scribe. Mul also uses this to broker new instances of Scribe. #include "ScribeSharedMem.h" //////////////////////////////////////////////////////////////////////////////////// // ScribeFunc void TraceTime(char *s); extern void LoadCalendarStringTable(); // Misc extern void WrapAndQuote(LStream &Out, const char *Quote, int Wrap, const char *Text, const char *Cp = NULL, const char *MimeType = NULL); extern void RemoveReturns(char *s); extern bool DecodeUuencodedAttachment(LDataStoreI *Store, LArray &Files, LStreamI *Out, const char *In); // extern LDocView *CreateIeControl(int Id); // Casters... extern Mail *IsMail(LListItem *Item); extern Contact *IsContact(LListItem *Item); extern Thing *CastThing(LDataI *t); // Folder extern void Scribe_Repair(ScribeWnd *Parent); // Scribe windows extern bool OpenPopView(ScribeWnd *Parent, LArray &Lst); extern void OpenFolderProperties(ScribeFolder *Parent, int Tab, std::function callback); extern LView *OpenFinder(ScribeWnd *App, ScribeFolder *Folder); // Import #include "Imp_Outlook.h" #include "Imp_Beos.h" extern void Import_NetscapeContacts(ScribeWnd *Parent); extern void Import_UnixMBox(ScribeWnd *Parent); extern void Import_OutlookExpress(ScribeWnd *Parent, bool v5 = true); extern void Import_EudoraAddressBook(ScribeWnd *App); extern void Import_MozillaAddressBook(ScribeWnd *App); extern void Import_MozillaMail(ScribeWnd *App); extern void Import_OutlookContacts(ScribeWnd *Parent); extern MailSource *NewOutlookMailSource(ScribeWnd *Parent, ScribeAccount *Account); // Export extern void Export_UnixMBox(ScribeWnd *Parent); extern void ImportCsv(ScribeWnd *App); extern void ExportCsv(ScribeWnd *App); extern void ImportEml(ScribeWnd *App); +extern void ExportScribe(ScribeWnd *App, LMailStore *Store); // DOM stuff extern void InitStrToDom(); #endif diff --git a/Code/ScribeUtils.cpp b/Code/ScribeUtils.cpp --- a/Code/ScribeUtils.cpp +++ b/Code/ScribeUtils.cpp @@ -1,2123 +1,2123 @@ #include "Scribe.h" #include "lgi/common/Http.h" #include "lgi/common/DocView.h" #include "lgi/common/Store3.h" #include "lgi/common/Button.h" #include "lgi/common/TableLayout.h" #include "lgi/common/TabView.h" #include "lgi/common/OpenSSLSocket.h" #include "lgi/common/LgiRes.h" #include "ScribeUtils.h" #include "ScribeDefs.h" #include "ScribeListAddr.h" #include "resdefs.h" #include "../src/common/Coding/ScriptingPriv.h" #define COMP_FUNCTIONS 1 #include "lgi/common/ZlibWrapper.h" static char Ws[] = " \t\r\n"; #include "chardet.h" LString DetectCharset(LString s) { DetectObj *obj = detect_obj_init (); if (!obj) return NULL; LString cs; if (detect_r(s.Get(), s.Length(), &obj) == CHARDET_SUCCESS) cs = obj->encoding; // obj->encoding, obj->confidence, obj->bom detect_obj_free (&obj); return cs; } const char *ScribeResourcePath() { static char Res[MAX_PATH_LEN] = {0}; if (!Res[0]) { #if defined(LINUX) // Check for AppImage location LFile::Path app(LSP_APP_INSTALL); app += "../../usr/share/applications"; if (app.Exists()) { strcpy_s(Res, sizeof(Res), app.GetFull()); LgiTrace("%s:%i - Res: %s.\n", _FL, Res); return Res; } // else LgiTrace("%s:%i - Warning: app image resource '%s' doesn't exist.\n", _FL, app.GetFull().Get()); // else fall through to portable mode #elif defined(MAC) // Find resource folder in app bundle LMakePath(Res, sizeof(Res), LGetExeFile(), "Contents/Resources"); return Res; #endif #if !defined(MAC) const char *Paths[] = { "./Resources", "../Resources", "../../Resources", }; bool Found = false; for (unsigned i=0; iGet(d, sizeof(d)); p.Push(d); break; } } } void PushArrayContent(LStringPipe &p, char *&s, LDom *Source) { // Process inside of array index while (s && *s) { // Skin ws while (*s && strchr(Ws, *s)) s++; // Is end of array brackets? if (*s == ']') { break; } else if (*s == '\'' || *s == '\"') { // String const char Delim = *s++; char *e = strchr(s, Delim); if (e) { p.Push(s, e-s); s = e + 1; } else { s += strlen(s); break; } } else { // Variable LStringPipe Var; char *e = s; int Depth = 0; while (*e && !strchr(Ws, *e)) { if (*e == '[') { e++; Var.Push(s, e-s); PushArrayContent(Var, e, Source); s = e; Depth++; } else if (*e == ']') { if (Depth > 0 || e[1] == '.') { // Continue the variable if (Depth) Depth--; e++; } else { // End the var break; } } else { e++; } } Var.Push(s, e-s); char *Tok = Var.NewStr(); if (Tok) { LVariant v; if (Source->GetValue(Tok, v)) { PushVariant(p, v); } else { p.Push(Tok); } DeleteArray(Tok); } s = e; } } } char *ScribeInsertFields(const char *Template, LDom *Source) { if (Template && Source) { LStringPipe p; char *n; for (const char *s=Template; s && *s; s = n) { n = strstr((char*)s, "') || strchr(Ws, *e) ) { break; } e++; } if (*e == '[') { e++; Var.Push(s, e-s); // Process inside of array index PushArrayContent(Var, e, Source); } else if (strchr(Ws, *e)) { // Skip whitespace Var.Push(s, e-s); while (*e && strchr(Ws, *e)) { e++; } } else if (e[0] == '?' && e[1] == '>') { // End of var Var.Push(s, e-s); GotEnd = true; n = e; break; } else { // error n = e;; break; } s = e; } if (GotEnd) { char *Name = Var.NewStr(); if (Name) { char *i = Name, *o = Name; while (*i) { if (*i == '=') { i++; char h[] = {i[0], i[1], 0}; *o++ = htoi(h); i += 2; } else { *o++ = *i++; } } *o++ = 0; LVariant v; if (Source->GetValue(Name, v)) { switch (v.Type) { default: break; case GV_STRING: { p.Push(v.Str()); break; } case GV_INT32: { char i[32]; sprintf_s(i, sizeof(i), "%i", v.Value.Int); p.Push(i); break; } case GV_DOUBLE: { char d[32]; sprintf_s(d, sizeof(d), "%f", v.Value.Dbl); p.Push(d); break; } case GV_DATETIME: { char d[64]; v.Value.Date->Get(d, sizeof(d)); p.Push(d); break; } } } DeleteArray(Name); } n += 2; } } else { p.Push(s); break; } } return p.NewStr(); } return 0; } ////////////////////////////////////////////////////////////////////////////////////// HttpImageThread::HttpImageThread(ScribeWnd *app, const char *proxy, LThreadTarget *First) : LThreadWorker(First, "HtmlImageLoader") { App = app; Proxy = proxy; Cache = ScribeTempPath(); if (!LDirExists(Cache)) FileDev->CreateFolder(Cache); } HttpImageThread::~HttpImageThread() { } void HttpImageThread::DoJob(LThreadJob *j) { LDocumentEnv::LoadJob *Job = dynamic_cast(j); if (!Job) return; char *d = strrchr(Job->Uri, '/'); if (!d) { Job->Status = LDocumentEnv::LoadJob::JobErr_Uri; Job->Error.Printf("No '/' in uri '%s'", Job->Uri.Get()); return; } LString CachedFile = UriMap.Find(Job->Uri); if (!CachedFile) { char *Ext = LGetExtension(++d); auto Qm = Ext ? strchr(Ext, '?') : NULL; auto Len = Qm ? Qm - Ext : Strlen(Ext); char p[MAX_PATH_LEN]; for (int i=0; i<1000; i++) { char Hash[256]; sprintf_s(Hash, sizeof(Hash), "%x_%i.%.*s", LHash((uchar*)d + 1, -1, true), i++, (int)Len, Ext); if (!LMakePath(p, sizeof(p), Cache, Hash)) { Job->Status = LDocumentEnv::LoadJob::JobErr_Path; Job->Error.Printf("MakePath failed: '%s' + '%s'", Cache.Get(), Hash); return; } if (!LFileExists(p)) break; } UriMap.Add(Job->Uri, CachedFile = p); } if (!LFileExists(CachedFile)) { const char *InHeaders = "User-Agent: Memecode Scribe\r\n" "Accept: text/html,application/xhtml+xml,application/xml,image/png,image/*;q=0.9,*/*;q=0.8\r\n" "Accept-Encoding: gzip, deflate\r\n"; LFile f; if (f.Open(CachedFile, O_READWRITE)) { LUri Prox(Proxy); bool r = LgiGetUri(this, &f, &Job->Error, Job->Uri, InHeaders, Proxy ? &Prox : NULL); f.Close(); if (!r) { Job->Status = LDocumentEnv::LoadJob::JobErr_GetUri; FileDev->Delete(CachedFile, false); } } else { Job->Status = LDocumentEnv::LoadJob::JobErr_FileOpen; } } if (LFileExists(CachedFile)) { LString::Array Mime = LGetFileMimeType(CachedFile).Split("/"); if (Mime[0].Equals("image")) { int Promote = GdcD->SetOption(GDC_PROMOTE_ON_LOAD, 0); Job->pDC.Reset(GdcD->Load(CachedFile)); GdcD->SetOption(GDC_PROMOTE_ON_LOAD, Promote); if (Job->pDC) { Job->Status = LDocumentEnv::LoadJob::JobOk; } else { char *d = strrchr(CachedFile, DIR_CHAR); Job->Error.Printf("%s:%i - LoadDC(%s) failed [%s].", _FL, d?d+1:CachedFile.Get(), Job->Uri.Get()); FileDev->Delete(CachedFile, false); Job->Status = LDocumentEnv::LoadJob::JobErr_ImageFilter; } } else { // Css??? LFile *f = new LFile; if (f) { if (f->Open(CachedFile, O_READ)) { Job->Stream.Reset(f); Job->Status = LDocumentEnv::LoadJob::JobOk; } else { Job->Status = LDocumentEnv::LoadJob::JobErr_FileOpen; Job->Error.Printf("%s:%i - Cant read from '%s' (err=%i).", _FL, CachedFile.Get(), f->GetError()); delete f; } } else Job->Status = LDocumentEnv::LoadJob::JobErr_NoMem; } } else if (!Job->Error) { Job->Status = LDocumentEnv::LoadJob::JobErr_NoCachedFile; Job->Error = "No file in cache"; } if (Job->Error) { LgiTrace("Image load failed: %s\n", Job->Error.Get()); } } char *ScribeTempPath() { static char Tmp[MAX_PATH_LEN] = ""; if (Tmp[0] == 0) { if (LGetSystemPath(LSP_TEMP, Tmp, sizeof(Tmp))) { LMakePath(Tmp, sizeof(Tmp), Tmp, "Scribe"); } else { LgiTrace("%s:%i - LgiGetSystemPath(LSP_TEMP) failed.\n", _FL); return NULL; } } if (!LDirExists(Tmp)) { LError Err; if (!FileDev->CreateFolder(Tmp, true, &Err)) { LgiTrace("%s:%i - CreateFolder(%s) failed with %i\n", _FL, Tmp, Err.GetCode()); return NULL; } } return Tmp; } void ClearTempPath() { char *Tmp = ScribeTempPath(); if (Tmp) { if (!LDirExists(Tmp)) FileDev->CreateFolder(Tmp); LDirectory d; for (int b = d.First(Tmp); b; b = d.Next()) { if (!d.IsDir()) { char p[256]; d.Path(p, sizeof(p)); FileDev->Delete(p, false); } } } } //////////////////////////////////////////////////////////////////////////////////////////////// #define BufferLen_64ToBin(l) ( ((l)*3)/4 ) #define BufferLen_BinTo64(l) ( ((((l)+2)/3)*4) ) int DecodeUuencodedChar(const char *&s) { int Status = -1; if (*s == 0x60) { Status = 0; s++; } else if (*s >= (' ' + 64) || *s < ' ') { printf("%s:%i - Invalid uuencode char: %c (%i)\n", _FL, *s, (uchar)*s); } else { Status = *s - ' '; s++; } return Status; } bool DecodeUuencodedLine(LStreamI *Out, const char *Text, ssize_t Len) { bool Status = false; if (Text && Len > 1) { uchar *Buf = new uchar[Len]; if (Buf) { const char *End = Text + Len; const char *c = Text; uchar *d = Buf; int Count = DecodeUuencodedChar(c); int Processed = 0; if (Count < 0) return false; while (c < End && *c) { int t[4]; // De-text t[0] = DecodeUuencodedChar(c); if (t[0] < 0) break; t[1] = DecodeUuencodedChar(c); if (t[1] < 0) break; t[2] = DecodeUuencodedChar(c); if (t[2] < 0) break; t[3] = DecodeUuencodedChar(c); if (t[3] < 0) break; // Convert to binary uchar b[3] = { (uchar) ((t[0] << 2) | ((t[1] & 0x30) >> 4)), (uchar) (((t[1] & 0xF) << 4) | ((t[2] & 0x3C) >> 2)), (uchar) (((t[2] & 0x3) << 6) | (t[3])) }; // Push onto the output stream switch (Count - Processed) { case 1: { *d++ = b[0]; Processed++; break; } case 2: { *d++ = b[0]; *d++ = b[1]; Processed += 2; break; } default: { if (Count - Processed >= 3) { *d++ = b[0]; *d++ = b[1]; *d++ = b[2]; Processed += 3; } break; } } } if (Processed != Count) { printf("%s:%i - uuencode line error, processed %i of %i\n", _FL, Processed, Count); } Status = Out->Write(Buf, d-Buf) > 0; DeleteArray(Buf); } } return Status; } bool DecodeUuencodedAttachment(LDataStoreI *Store, LArray &Files, LStreamI *Out, const char *In) { if (Store && In) { // const char Ws[] = " \t\r\n"; LStringPipe FileName; LAutoPtr FileData; const char *e; const char *Last = In; int Line = 1; for (const char *s = In; s && *s; s = *e?e+1:e) { // Find the end of the line... e = s; while (*e && *e != '\n') e++; if (FileData) { if (_strnicmp(s, "end", 3) == 0) { // Write attachment LDataI *Attachment = Store->Create(MAGIC_ATTACHMENT); if (Attachment) { LAutoString Name(FileName.NewStr()); if (Name) { char *e = Name + strlen(Name); while (e > Name.Get() && strchr(" \t\r\n", e[-1])) *--e = 0; Attachment->SetStr(FIELD_NAME, Name); } LAutoStreamI fd(FileData.Release()); Attachment->SetStream(fd); Files.Add(Attachment); } FileData.Reset(); } else if (!DecodeUuencodedLine(FileData, s, e - s)) { /* printf("%s:%i - DecodeUuencodedLine failed on line %i:\n\t%s\n", _FL, Line, s); */ } } // Is it the start of a file else if (_strnicmp(s, "begin ", 6) == 0) { if (Last) { Out->Write(Last, s - Last); Last = 0; } LToken Header(s, " ", true, e - s); if (Header.Length() >= 3) { LMemQueue File; for (int n=2; Header[n]; n++) { FileName.Print("%s%s", n==2?"":" ", Header[n]); } } FileData.Reset(new LStringPipe(256)); } else if (!Last) { Last = s; } Line++; } if (Files.Length() && Last) { Out->Write(Last, strlen(Last)); } } return Files.Length() > 0; } char *MakeFileName(const char *ContentUtf, const char *Ext) { if (!ContentUtf) { LAssert(!"Invalid parameter."); return 0; } char *Content = 0; if (LIsUtf8(ContentUtf)) { // Valid UTF-8 Content = NewStr(ContentUtf); } else { // Garbage, so just ignore the input data. char n[256]; sprintf_s(n, sizeof(n), "_%i", LRand(1000000)); Content = NewStr(n); } if (!Content) { LAssert(!"No content to make filename from."); return 0; } char File[MAX_PATH_LEN]; char *e = Content; for (int i=0; i<64 && *e; i++) { char *before = e; e = LSeekUtf8(e, 1); if (e == before) { LAssert(!"LSeekUtf8 failed to more pointer forward."); break; } } *e = 0; if (strlen(Content) > 0) { if (Ext) sprintf_s(File, sizeof(File), "%s.%s", Content, Ext); else sprintf_s(File, sizeof(File), "%s", Content); } else { LAssert(!"No content for file name?"); strcpy_s(File, sizeof(File), "file"); } // Strip out invalid characters... char *Out = File; for (char *In = File; *In; In++) { if (!strchr("\\/?*:\"<>|\r\n", *In)) { *Out++ = *In; } } *Out++ = 0; LAssert(strlen(File) > 0); char Temp[MAX_PATH_LEN]; LMakePath(Temp, sizeof(Temp), ScribeTempPath(), File); if (LFileExists(Temp)) { char *Dot = strrchr(Temp, '.'); for (int i=2; LFileExists(Temp); i++) { ssize_t Len = Dot - Temp; sprintf_s(Dot, sizeof(Temp)-Len, "%i.%s", i, Ext); } } DeleteArray(Content); return NewStr(Temp); } /////////////////////////////////////////////////////////////////////////////////////////// Store3Progress::Store3Progress(LView *parent, bool interact) : LProgressDlg(parent) { Interact = interact; NewFormat = -1; } const char *Store3Progress::GetStr(int id) { switch (id) { case Store3UiError: return Err; case Store3UiStatus: return (Cache = ItemAt(0)->GetDescription()); } LAssert(0); return 0; } Store3Status Store3Progress::SetStr(int id, const char *str) { switch (id) { case Store3UiError: Err = str; // Fall through case Store3UiStatus: ItemAt(0)->SetDescription(str); return Store3Success; } LAssert(0); return Store3Error; } int64 Store3Progress::GetInt(int id) { switch (id) { case Store3UiCurrentPos: return ItemAt(0)->Value(); case Store3UiInteractive: return Interact; case Store3UiCancel: return IsCancelled(); case Store3UiNewFormat: return NewFormat; } LAssert(0); return -1; } Store3Status Store3Progress::SetInt(int id, int64 i) { switch (id) { case Store3UiCancel: return Store3Error; case Store3UiCurrentPos: ItemAt(0)->Value(i); break; case Store3UiMaxPos: ItemAt(0)->SetRange(i); break; case Store3UiNewFormat: NewFormat = (int)i; break; default: LAssert(0); return Store3Error; } return Store3Success; } /////////////////////////////////////////////////////////////////////// class BufferedTrace { List Traces; public: ~BufferedTrace() { for (auto s: Traces) { LgiTrace(s); DeleteArray(s); } } void Trace(char *s) { if (s) { Traces.Insert(NewStr(s)); } } } ; static BufferedTrace Bt; void TraceTime(char *s) { static int64 Last = 0; if (s) { int64 Now = LCurrentTime(); int64 Diff = 0; if (Last) { Diff = Now - Last; } else { Diff = 0; } Last = Now; char m[256]; sprintf_s(m, sizeof(m), "%s (+%i)", s, (int)Diff); Bt.Trace(m); } else { Last = 0; } } ///////////////////////////////////////////////////////////////////////////// Counter::~Counter() { for (auto c: *this) { DeleteObj(c); } } CountItem *Counter::FindType(int Type) { for (auto c: *this) { if (Type == c->Type) { return c; } } CountItem *c = new CountItem; if (c) { c->Type = Type; Insert(c); } return c; } void Counter::Inc(int Type) { CountItem *c = FindType(Type); if (c) { c->Count++; } } void Counter::Dec(int Type) { CountItem *c = FindType(Type); if (c) { c->Count--; } } void Counter::Add(int Type, int64 n) { CountItem *c = FindType(Type); if (c) { c->Count += n; } } void Counter::Sub(int Type, int64 n) { CountItem *c = FindType(Type); if (c) { c->Count -= n; } } int64 Counter::GetTypeCount(int Type) { CountItem *c = FindType(Type); if (c) { return c->Count; } return 0; } ////////////////////////////////////////////////////////// ItemFieldDef *ScribeGetFieldDefs(int Type) { switch ((uint32_t)Type) { case MAGIC_MAIL: { return MailFieldDefs; } case MAGIC_CONTACT: { return ContactFieldDefs; } case MAGIC_CALENDAR: { return CalendarFields; } } return 0; } Contact *IsContact(LListItem *Item) { return dynamic_cast(Item); } Mail *IsMail(LListItem *Item) { return dynamic_cast(Item); } ////////////////////////////////////////////////////////////////////////////// char sMimeVCard[] = "text/x-vcard"; char sMimeVCalendar[] = "text/calendar"; char sMimeICalendar[] = "application/ics"; char sMimeMbox[] = "text/mbox"; char sMimeLgiResource[] = "application/x-lgi-resource"; char sMimeMessage[] = "message/rfc822"; char sMimeXml[] = "text/xml"; LString ScribeGetFileMimeType(const char *File) { LString Ret; if (File) { char *Ext = LGetExtension((char*)File); if (Ext) { if (_stricmp(Ext, "lr8") == 0) { Ret = sMimeLgiResource; } else if (_stricmp(Ext, "ici") == 0) { Ret = "application/x-ici"; } else if (_stricmp(Ext, "vcf") == 0) { Ret = sMimeVCard; } else if (_stricmp(Ext, "vcs") == 0 || _stricmp(Ext, "ics") == 0) { Ret = sMimeVCalendar; } else if (_stricmp(Ext, "eml") == 0) { Ret = sMimeMessage; } #if defined WIN32 // Hard code extensions (because windows doesn't get it right) else if (_stricmp(Ext, "mbx") == 0 || _stricmp(Ext, "mbox") == 0) { Ret = sMimeMbox; } #endif } if (!Ret) { // Do normal lookup Ret = LGetFileMimeType(File); } } return Ret; } ///////////////////////////////////////////////////////////////////// Mailto::Mailto(ScribeWnd *app, const char *s) { App = app; Subject = NULL; Body = NULL; if (!s) return; // Do some detection of what type of string this is... // // Could be in various formats: // 1. user@isp.com // 2. user@isp.com, user2@isp.com, user3@isp.com // 3. "First Last" // 4. "First Last" , "First2 Last2" // 5. mailto:user@isp.com // 6. mailto:user@isp.com?subject=xxxxxx&body=xxxxxxxx // Skip whitespace while (*s && strchr(" \t\r\n", *s)) s++; // Check for mailto prefix if (_strnicmp(s, "mailto:", 7) == 0) { // Parse mailto URI char *e = NewStr(s + 7); char *In, *Out = e; for (In = e; *In; ) { if (In[0] == '%' && In[1] && In[2]) { char h[3] = { In[1], In[2], 0 }; *Out++ = htoi(h); In += 3; } else { *Out++ = *In++; } } *Out++ = 0; // Process mailto syntax char *Question = strchr(e, '?'); if (Question) { *Question++ = 0; // Split all the headers up LToken Headers(Question, "&"); for (unsigned h=0; hsAddr = e; To.Insert(la); } } DeleteArray(e); } else { // Not a mailto, apply normal email recipient parsing char White[] = " \t\r\n"; #define SkipWhite(s) while (*s && strchr(White, *s)) s++; const char *Addr = s; for (const char *c = s; true;) { SkipWhite(c); if (*c == '\'' || *c == '\"') { char Delim = *c++; char *e = strchr((char*)c, Delim); if (e) c = e + 1; else c += strlen(c); } else if (*c == '<') { char *e = strchr((char*)c, '>'); if (e) c = e + 1; else c++; } else if (*c == ',' || *c == 0) { char *a = NewStr(Addr, c - Addr); if (a) { LAutoString Name, Addr; DecodeAddrName(a, Name, Addr, 0); if (Name || Addr) { ListAddr *la = new ListAddr(App); if (la) { if (Name && Addr) { la->sName = Name.Get(); la->sAddr = Addr.Get(); } else { la->sAddr = Name ? Name.Get() : Addr.Get(); } To.Insert(la); } } DeleteArray(a); } if (!*c) break; else { c++; SkipWhite(c); Addr = c; } } else c++; } } } Mailto::~Mailto() { To.DeleteObjects(); DeleteArray(Subject); DeleteArray(Body); } void Mailto::Apply(Mail *m) { if (m) { bool Dirty = false; if (Subject) { m->SetSubject(Subject); Dirty = true; } if (Body) { LVariant HtmlEdit; m->App->GetOptions()->GetValue(OPT_EditControl, HtmlEdit); auto Email = m->GetFromStr(FIELD_EMAIL); ScribeAccount *Acc = m->App->GetAccountByEmail(Email); if (!Acc) Acc = m->App->GetCurrentAccount(); LVariant Sig; LString Content; if (Acc) { if (HtmlEdit.CastInt32()) { Sig = Acc->Identity.HtmlSig(); if (Sig.Str()) { char *s = Sig.Str(); char *e = stristr(s, ""); if (e) Content.Printf("%.*s\n%s\n%s", e - s, s, Body, e + 6); else Content.Printf("%s\n%s", Body, s); } else Content = Body; } else { Sig = Acc->Identity.TextSig(); Content.Printf("%s\n%s", Body, Sig.Str()); } } if (HtmlEdit.CastInt32()) m->SetHtml(Content); else m->SetBody(Content); Dirty = true; } for (auto t: To) { - GDataIt To = m->GetObject()->GetList(FIELD_TO); + LDataIt To = m->GetObject()->GetList(FIELD_TO); if (To) { LDataPropI *Addr = To->Create(m->GetObject()->GetStore()); if (Addr) { LDataPropI *p = dynamic_cast(t); if (p) { Addr->CopyProps(*p); To->Insert(Addr); } else { LAssert(!"Not the right object."); DeleteObj(Addr); } } if (m->GetUI()) { m->GetUI()->AddRecipient(new ListAddr(App, t)); } Dirty = true; } } if (Dirty) m->SetDirty(); } } ScribeDom::ScribeDom(ScribeWnd *a) { App = a; Email = NULL; Con = NULL; Cal = NULL; Fil = NULL; Grp = NULL; } bool ScribeDom::GetVariant(const char *Name, LVariant &Value, const char *Array) { ScribeDomType Fld = StrToDom(Name); switch (Fld) { case SdScribe: // Type: ScribeWnd { Value = (LDom*)App; break; } case SdMail: // Type: Mail { Value = Email; break; } case SdContact: // Type: Contact { Value = Con; break; } case SdContactGroup: // Type: ContactGroup { Value = Grp; break; } case SdCalendar: // Type: Calendar { Value = Cal; break; } case SdFilter: // Type: Filter { Value = Fil; break; } case SdNow: // Type: String { char n[256]; LDateTime Now; Now.SetNow(); Now.Get(n, sizeof(n)); Value = n; break; } default: { return false; } } return true; } ////////////////////////////////////////////////////////////////////////////////////// #include "lgi/common/Html.h" #include "lgi/common/Button.h" class HtmlMsg : public LDialog, public LDefaultDocumentEnv { LTableLayout *Tbl; Html1::LHtml *Html2; public: HtmlMsg(LViewI *Parent, const char *Html, const char *Title, int Type) { LPoint Size(200, 200); Tbl = NULL; SetParent(Parent); Name(Title?Title:"Message"); AddView(Tbl = new LTableLayout(2222)); auto c = Tbl->GetCell(0, 0); if (c->Add(Html2 = new Html1::LHtml(100, 0, 0, (int)(GdcD->X() * 0.5), (int)(GdcD->Y() * 0.75), this))) { Html2->SetCharset("utf-8"); Html2->Name(Html); Size = Html2->Layout(); LRect r(0, 0, Size.x, Size.y); Html2->SetPos(r); } LArray Btns; switch (Type & 0xf) { case MB_OK: Btns.Add(new LButton(IDOK, 0, 0, -1, -1, "Ok")); break; case MB_OKCANCEL: Btns.Add(new LButton(IDOK, 0, 0, -1, -1, "Ok")); Btns.Add(new LButton(IDCANCEL, 0, 0, -1, -1, "Cancel")); break; case MB_YESNO: Btns.Add(new LButton(IDYES, 0, 0, -1, -1, "Yes")); Btns.Add(new LButton(IDNO, 0, 0, -1, -1, "No")); break; case MB_YESNOCANCEL: Btns.Add(new LButton(IDYES, 0, 0, -1, -1, "Yes")); Btns.Add(new LButton(IDNO, 0, 0, -1, -1, "No")); Btns.Add(new LButton(IDCANCEL, 0, 0, -1, -1, "Cancel")); break; } c = Tbl->GetCell(0, 1); c->TextAlign(LCss::AlignCenter); for (auto b: Btns) c->Add(b); LRect r(0, 0, Size.x + 20 + LAppInst->GetMetric(LGI_MET_DECOR_X), Size.y + 20 + LSysFont->GetHeight() + LAppInst->GetMetric(LGI_MET_DECOR_CAPTION) + LAppInst->GetMetric(LGI_MET_DECOR_Y)); SetPos(r); MoveSameScreen(Parent); } int OnNotify(LViewI *Ctrl, LNotification n) { switch (Ctrl->GetId()) { case IDOK: case IDCANCEL: case IDYES: case IDNO: EndModal(Ctrl->GetId()); break; } return LDialog::OnNotify(Ctrl, n); } }; void LHtmlMsg(std::function Callback, LViewI *Parent, const char *Html, const char *Title, int Type, ...) { va_list Arg; va_start(Arg, Type); #undef vsnprintf int length = vsnprintf(NULL, 0, Html, Arg); LAutoString Msg(new char[++length]); vsprintf_s(Msg, length, Html, Arg); va_end(Arg); auto Dlg = new HtmlMsg(Parent, Msg, Title, Type); Dlg->DoModal([Callback](auto dlg, auto id) { if (Callback) Callback(id); delete dlg; }); } ///////////////////////////////////////////////////////////////////////////// void TabDialog::OnCreate() { LTabView *Tab; if (GetViewById(TabCtrlId, Tab)) { Tab->SetPourChildren(true); LRect r(0, 0, 100, 100); Tab->SetPos(r); } OnPosChange(); } void TabDialog::IdealSize(LButton *b) { LViewLayoutInfo Inf; if (b->OnLayout(Inf)) { b->OnLayout(Inf); } else if (b->GetWindow()) { auto s = b->GetWindow()->GetDpiScale(); LDisplayString ds(b->GetFont(), b->Name()); Inf.Width.Max = (int32)(ds.X() + (s.x * LButton::Overhead.x)); Inf.Height.Max = (int32)(ds.Y() + (s.y * LButton::Overhead.y)); } else { LAssert(!"No way to set ideal size."); return; } LRect p = b->GetPos(); p.SetSize(Inf.Width.Max, Inf.Height.Max); b->SetPos(p); } void TabDialog::OnPosChange() { LButton *Ok = 0, *Cancel = 0, *Help = 0; LViewI *Tab = 0; if (GetViewById(TabCtrlId, Tab) && GetViewById(IDOK, Ok) && GetViewById(IDCANCEL, Cancel)) { GetViewById(HelpBtnId, Help); LRect r = GetClient(); r.Inset(LTableLayout::CellSpacing, LTableLayout::CellSpacing); IdealSize(Ok); IdealSize(Cancel); LRect t = r; t.y2 -= LTableLayout::CellSpacing + Ok->Y(); Tab->SetPos(t); if (Help) { IdealSize(Help); LRect h = Help->GetPos(); h.Offset(r.x1 - h.x1, r.y2 - h.Y() + 1 - h.y1); Help->SetPos(h); } LRect c = Cancel->GetPos(); c.Offset(r.x2 - c.X() + 1 - c.x1, r.y2 - c.Y() + 1 - c.y1); Cancel->SetPos(c); LRect o = Ok->GetPos(); o.Offset(c.x1 - LTableLayout::CellSpacing - o.X() + 1 - o.x1, r.y2 - o.Y() + 1 - o.y1); Ok->SetPos(o); } } LAutoString ConvertThreadIndex(char *ThreadIndex, int TruncateChars) { LAutoString a; if (ThreadIndex) { uchar InBuf[256]; ssize_t In = ConvertBase64ToBinary(InBuf, sizeof(InBuf), ThreadIndex, strlen(ThreadIndex)); LAssert(In >= 22); LStringPipe OutBuf(256); for (int i=0; i, ScribeDomType> Scribe_StrToDom(0, SdNone); static LHashTbl, const char *> Scribe_DomToStr; void InitStrToDom() { if (Scribe_StrToDom.Length() == 0) { #undef _ #define _(name) Scribe_StrToDom.Add(#name, Sd##name); \ LAssert(Scribe_StrToDom.Find(#name) == Sd##name); \ Scribe_DomToStr.Add(Sd##name, #name); #include "DomTypeValues.h" #undef _ } } ScribeDomType StrToDom(const char *s) { ScribeDomType d = Scribe_StrToDom.Find(s); return d; } const char *DomToStr(ScribeDomType d) { const char *s = Scribe_DomToStr.Find(d); return s; } void PatternBox(LSurface *pDC, const LRect &r) { int All = r.X() + r.Y() - 1; int MinEdge = MIN(r.X(), r.Y()); bool Wider = r.X() > r.Y(); for (int i=0; i> 2) % 2; /* if (i <= 4) LgiTrace("pt=%i,%i draw=%i\n", pt.x, pt.y, Draw); */ if (!Draw) continue; if (i < MinEdge) { pDC->Line(r.x1, r.y1 + i, r.x1 + i, r.y1); } else if (Wider) { if (i < r.X()) { int yy = r.Y() - 1; pDC->Line(pt.x, pt.y, pt.x - yy, pt.y + yy); } else { int yy = r.Y() - (i - r.X() + 1) - 1; pDC->Line(r.x2, r.y2-yy, r.x2-yy, r.y2); } } else // Tall { if (i < r.Y()) { int xx = r.X() - 1; pDC->Line(r.x1, r.y1 + i, r.x1 + xx, r.y1 + i - xx); } else { int xx = r.X() - (i - r.Y() + 1) - 1; pDC->Line(r.x2-xx, r.y2, r.x2, r.y2-xx); } } } } /////////////////////////////////////////////////////////////////////////////////////// ContactGroup *LookupContactGroup(ScribeWnd *App, const char *Name) { auto Srcs = App->GetThingSources(MAGIC_GROUP); if (!Srcs.Length() || !Name) return NULL; for (auto s: Srcs) { s->LoadThings(); for (auto t: s->Items) { ContactGroup *g = t->IsGroup(); if (!g) continue; LVariant Nm; if (g->GetVariant("Name", Nm) && Nm.Str() && _stricmp(Nm.Str(), Name) == 0) { return g; } } } return NULL; } ////////////////////////////////////////////////////////////////////////////////////////////////// LOAuth2::Params GetOAuth2Params(const char *Host, Store3ItemTypes Context) { LOAuth2::Params p; // FYI: None of this works due to issues at the providers end. It did sometime in the // past. And is only here in case someone wants to try and get it working again. if (stristr(Host, "google.") || stristr(Host, "gmail.")) { if (Context == MAGIC_MAIL) { p.AuthUri = "https://accounts.google.com/o/oauth2/auth"; p.ApiUri = "https://www.googleapis.com/oauth2/v3/token"; #if 1 // Old scope: p.Scope = "https://mail.google.com/"; #else // New scope: (doesn't work) p.Scope = "https://www.googleapis.com/auth/gmail.modify"; #endif // p.RevokeUri = "https://accounts.google.com/o/oauth2/revoke"; } /* else if (Context == MAGIC_CALENDAR) { p.AuthUri = "https://accounts.google.com/o/oauth2/v2/auth"; p.ApiUri = "https://apidata.googleusercontent.com/caldav/v2/%s/user"; p.Scope = "https://www.googleapis.com/auth/calendar"; } */ else return p; p.Provider = LOAuth2::Params::OAuthGoogle; p.ClientID = ""; p.ClientSecret = ""; p.RedirURIs = "urn:ietf:wg:oauth:2.0:oob\nhttp://localhost"; } else if (stristr(Host, "outlook.") && !stristr(Host, "office365.")) { if (Context == MAGIC_MAIL) { p.RedirURIs = "urn:ietf:wg:oauth:2.0:oob\nhttp://localhost"; p.AuthUri = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize"; p.ApiUri = "https://login.microsoftonline.com/common/oauth2/v2.0/token"; } else return p; p.Provider = LOAuth2::Params::OAuthMicrosoft; p.ClientID = ""; p.ClientSecret = ""; p.Scope = "https://outlook.office.com/mail.readwrite%20https://outlook.office.com/mail.send"; } return p; } ////////////////////////////////////////////////////////////////////////////////////////////////// class ScribeHtmLParser : public LHtmlParser { LScriptEngine Eng; public: struct HtmlElem : public LHtmlElement { LHashTbl, LString> Attr; HtmlElem(LHtmlElement *e) : LHtmlElement(e) { } bool GetVariant(const char *Name, LVariant &Value, const char *Array = NULL) { if (!Stricmp(Name, "element")) { Value = Tag.Get(); return true; } else if (!Stricmp(Name, "content")) { Value.OwnStr(WideToUtf8(Txt.Get())); return true; } else if (!Stricmp(Name, "attr")) { if (Array) { char *s = Attr.Find(Array); if (s) { Value = s; return true; } } } return false; } bool Get(const char *attr, const char *&val) { auto s = Attr.Find(attr); if (!s) return false; val = s.Get(); return true; } void Set(const char *attr, const char *val) { Attr.Add(attr, val); } }; ScribeHtmLParser() : LHtmlParser(NULL), Eng(NULL, NULL, NULL) { } LHtmlElement *CreateElement(LHtmlElement *Parent) { return new HtmlElem(Parent); } void Evaluate(LArray &Out, LString Search, HtmlElem *Elem) { LVariant Result; if (Eng.EvaluateExpression(&Result, Elem, Search)) { if (Result.CastInt32()) Out.Add(Elem); } for (auto e: Elem->Children) Evaluate(Out, Search, dynamic_cast(e)); } }; bool SearchHtml(LVariant *ReturnValue, const char *Html, const char *SearchExp, const char *ResultExp) { ScribeHtmLParser Parser; ScribeHtmLParser::HtmlElem Root(NULL); if (!Parser.Parse(&Root, Html)) { LgiTrace("%s:%i - HTML parsing failed.\n", _FL); *ReturnValue = false; return false; } LArray Matches; Parser.Evaluate(Matches, SearchExp, &Root); if (!ReturnValue->SetList()) return false; LScriptEngine Eng(NULL, NULL, NULL); for (auto e: Matches) { LVariant *Result = new LVariant; Eng.EvaluateExpression(Result, e, ResultExp); ReturnValue->Add(Result); } return true; } ////////////////////////////////////////////////////////////////////////////////////////////////////////// ScriptDownloadContentThread::ScriptDownloadContentThread(ScribeWnd *app, LString uri, LString callbackName, LVariant *userData) : LThread("ScriptDownloadContentThread", (App = app)->AddDispatch()) { Uri = uri; CallbackName = callbackName; if (userData) UserData = *userData; DeleteOnExit = true; Run(); } int ScriptDownloadContentThread::Main() { Result = LgiGetUri(this, &Out, &Err, Uri); return false; } void ScriptDownloadContentThread::OnComplete() { auto Cb = App->GetCallback(CallbackName); if (!Cb.Func) return; LVirtualMachine Vm; LScriptArguments Args(&Vm); LVariant vApp((LDom*)App); Args.Add(&vApp); LVariant vUri = Uri.Get(); Args.Add(&vUri); LVariant vResult = Result; Args.Add(&vResult); LVariant vData; if (Result) vData.OwnStr(Out.NewStr()); else vData = Err.Get(); Args.Add(&vData); Args.Add(&UserData); App->ExecuteScriptCallback(Cb, Args); } //////////////////////////////////////////////////////////////////////////////////////////////////////// // This converts an async call to sync, because the GetVariant / CallMethod API // can't be changed to include a callback. It's a hack until such time as there // is proper support for callbacks in the DOM api. void WaitForVariant(LVariant &var) { auto StartTs = LCurrentTime(); while (var.Type == GV_NULL) { LSleep(10); LYield(); if (LCurrentTime() - StartTs > 20000) { LgiTrace("%s:%i - WaitForVariant waiting for: %is", _FL, (int)(LCurrentTime()-StartTs)); StartTs = LCurrentTime(); } } } void WaitForString(LString &var) { auto StartTs = LCurrentTime(); while (var.Get() == NULL) { LSleep(10); LYield(); if (LCurrentTime() - StartTs > 20000) { LgiTrace("%s:%i - WaitForString waiting for: %is", _FL, (int)(LCurrentTime()-StartTs)); StartTs = LCurrentTime(); } } } diff --git a/Code/Store3Common.cpp b/Code/Store3Common.cpp --- a/Code/Store3Common.cpp +++ b/Code/Store3Common.cpp @@ -1,532 +1,532 @@ #include #include "lgi/common/Lgi.h" #include "lgi/common/Mail.h" #include "Store3Common.h" #include "ScribeInc.h" #include "DomType.h" LHashTbl,LDataStoreI*> LDataStoreI::Map; ////////////////////////////////////////////////////////////////////////////////////// const char *Store3ItemTypeToMime(Store3ItemTypes type) { switch (type) { case MAGIC_MAIL: return "message/rfc822"; case MAGIC_CONTACT: return "text/vcard"; case MAGIC_ATTACHMENT: return "application/octet-stream"; case MAGIC_CALENDAR: return "text/vcalendar"; case MAGIC_FILTER: return "text/x-email-filter"; default: LAssert(!"Unknown type"); break; } return NULL; } ////////////////////////////////////////////////////////////////////////////////////// LDataUserI::LDataUserI() { Object = NULL; } LDataUserI::~LDataUserI() { if (Object) Object->UserData = NULL; } LDataI *LDataUserI::GetObject() { return Object; } bool LDataUserI::SetObject(LDataI *o, bool InDestuctor, const char *File, int Line) { if (o == Object) return true; if (Object) { Object->UserData = NULL; if (!InDestuctor && Object->IsOrphan()) delete Object; Object = NULL; } Object = o; if (File) SetterRef.Printf("%s:%i", File, Line); else SetterRef.Empty(); if (Object) Object->UserData = this; return true; } ////////////////////////////////////////////////////////////////////////////// Store3Addr::Store3Addr(LDataStoreI *store, LDataPropI *i) { LAssert(store != NULL); Store = store; CC = 0; if (i) CopyProps(*i); } Store3Addr::~Store3Addr() { } size_t Store3Addr::Sizeof() { size_t s = sizeof(*this); if (Addr) s += strlen(Addr); if (Name) s += strlen(Name); return s; } void Store3Addr::SetStore(LDataStoreI *s) { Store = s; LAssert(Store != NULL); } void Store3Addr::Empty() { Name.Empty(); Addr.Empty(); CC = 0; } Store3CopyImpl(Store3Addr) { Empty(); Name = p.GetStr(FIELD_NAME); Addr = p.GetStr(FIELD_EMAIL); CC = (int)p.GetInt(FIELD_CC); return true; } const char *Store3Addr::GetStr(int id) { switch (id) { case FIELD_NAME: return Name; case FIELD_EMAIL: return Addr; } return 0; } Store3Status Store3Addr::SetStr(int id, const char *str) { switch (id) { case FIELD_NAME: { Name = str; break; } case FIELD_EMAIL: { Addr = str; break; } default: return Store3Error; } return Store3Success; } bool Store3Addr::GetVariant(const char *n, LVariant &Value, const char *Array) { ScribeDomType Fld = StrToDom(n); switch (Fld) { case SdName: // Type: String { Value = Name; break; } case SdEmail: // Type: String { Value = Addr; break; } case SdDomain: // Type: String { char *At = Addr ? strchr(Addr, '@') : NULL; if (At) Value = At + 1; else Value.Empty(); break; } case SdText: // Type: String { char s[512]; LString EscName; if (Name) EscName = LString::Escape(Name, -1, "\'"); if (Name && Addr) sprintf_s(s, sizeof(s), "\"%s\" <%s>", EscName.Get(), (char*)Addr); else if (Name) sprintf_s(s, sizeof(s), "\"%s\"", EscName.Get()); else if (Addr) sprintf_s(s, sizeof(s), "<%s>", (char*)Addr); else return false; s[sizeof(s)-1] = 0; Value = s; break; } case SdContact: // Type: Contact { LDataEventsI *e = Store->GetEvents(); if (!e) return false; LArray Matches; if (!e->Match(Store, this, MAGIC_CONTACT, Matches)) return false; Value = Matches[0]; break; } case SdGroups: // Type: String[] { LDataEventsI *e = Store->GetEvents(); if (!e) return false; LArray Matches; if (e->Match(Store, this, MAGIC_GROUP, Matches)) { Value.SetList(); for (unsigned i=0; i v(new LVariant); if (Matches[i]->GetValue("Name", *v)) Value.Value.Lst->Insert(v.Release()); } } else Value.Empty(); break; } default: { LAssert(!"Not a supported field."); return false; } } return true; } bool Store3Addr::SetVariant(const char *n, LVariant &Value, const char *Array) { ScribeDomType Fld = StrToDom(n); switch (Fld) { case SdName: // Type: String { Name = Value.Str(); break; } case SdEmail: // Type: String { Addr = Value.Str(); break; } case SdText: // Type: String { DecodeAddrName(Value.Str(), [&](LString name, LString addr){ Name = LString::UnEscape(name); Addr = addr; }, NULL); break; } default: { LAssert(!"Not a valid field"); return false; } } return true; } int64 Store3Addr::GetInt(int id) { switch (id) { case FIELD_CC: return CC; } return -1; } Store3Status Store3Addr::SetInt(int id, int64 i) { switch (id) { case FIELD_CC: CC = (int)i; break; default: return Store3Error; } return Store3Success; } /////////////////////////////////////////////////////////////////////////////////// Store3Field::Store3Field(LDataStoreI *Store, int id, int width) { Id = id; Width = width; } const char *Store3Field::GetStr(int id) { return 0; } int64 Store3Field::GetInt(int id) { switch (id) { case FIELD_ID: return Id; case FIELD_WIDTH: return Width; } return -1; } Store3Status Store3Field::SetInt(int id, int64 i) { switch (id) { case FIELD_ID: Id = (int)i; break; case FIELD_WIDTH: Width = (int)i; break; default: return Store3Error; } return Store3Success; } //////////////////////////////////////////////////////////////////////////////// // Mime conversion bool Store3ToGMime(LMime *Out, LDataPropI *InInterface) { LDataI *In = dynamic_cast(InInterface); if (!Out || !In) { LAssert(0); return false; } int Type = In->Type(); if (Type == MAGIC_MAIL) { - GDataIt Sub = In->GetList(FIELD_MIME_SEG); + LDataIt Sub = In->GetList(FIELD_MIME_SEG); LDataPropI *Child = Sub->First(); if (Child) { if (!Store3ToGMime(Out, Child)) return false; } else LAssert(0); } else if (Type == MAGIC_ATTACHMENT) { auto Hdrs = In->GetStr(FIELD_INTERNET_HEADER); if (Hdrs) { if (!Out->SetHeaders(Hdrs)) { LAssert(0); return false; } } else { // No headers??? } LAutoStreamI Data = In->GetStream(_FL); if (Data) { if (!Out->SetData(true, Data.Release())) { LAssert(0); return false; } } - GDataIt Sub = In->GetList(FIELD_MIME_SEG); + LDataIt Sub = In->GetList(FIELD_MIME_SEG); for (LDataPropI *Child = Sub->First(); Child; Child = Sub->Next()) { LMime *NewSeg = Out->NewChild(); if (NewSeg) { if (Store3ToGMime(NewSeg, Child)) Out->Insert(NewSeg); else return false; } else { LAssert(0); return false; } } } else { LAssert(!"Incorrect object type."); return false; } return true; } bool GMimeToStore3(LDataPropI *Out, LMime *In, bool InMemOnly) { LDataI *DataOut = dynamic_cast(Out); if (!DataOut || !In) { LAssert(0); return false; } if (!Out->SetStr(FIELD_INTERNET_HEADER, In->GetHeaders())) { LAssert(0); return false; } if (In->GetLength() > 0) { LAutoStreamI Data(new LMemStream(In->GetData(), -1, -1)); if (!Data) { LAssert(0); return false; } if (!DataOut) { LAssert(0); return false; } DataOut->SetStream(Data); } for (int i=0; iLength(); i++) { LDataI *cOut = DataOut->GetStore()->Create(MAGIC_ATTACHMENT); if (!cOut) return false; LMime *cIn = (*In)[i]; if (!GMimeToStore3(cOut, cIn)) return false; Store3Status s = cOut->Save(DataOut); if (s == Store3Error) { LAssert(0); return false; } } return true; } //////////////////////////////////////////////////////////////////////////////////////// LString HeadersFromStream(LStreamI *Msg) { LString s; for (int Sz = 1024; Msg && Sz < (64 << 10); Sz += 1024) { Msg->SetPos(0); if (!s.Length(Sz)) break; ssize_t Rd = Msg->Read(s.Get(), s.Length()); if (Rd <= 0) { s.Empty(); break; } s.Length(Rd); ptrdiff_t EndOfHeader = s.Find("\r\n\r\n"); if (EndOfHeader > 0) { s.Length(EndOfHeader); break; } } return s; } //////////////////////////////////////////////////////////////////////////////////////// LString CreateMboxHeader(LDataI *Object) { LString s; LDataPropI *From; if (!Object || !(From = Object->GetObj(FIELD_FROM))) { LAssert(0); return s; } // generate from header s.Printf("From %s ", From->GetStr(FIELD_EMAIL)); struct tm Ft; ZeroObj(Ft); LDateTime Rec = *Object->GetDate(FIELD_DATE_RECEIVED); if (!Rec.Year()) Rec.SetNow(); Ft.tm_sec = Rec.Seconds(); /* seconds after the minute - [0,59] */ Ft.tm_min = Rec.Minutes(); /* minutes after the hour - [0,59] */ Ft.tm_hour = Rec.Hours(); /* hours since midnight - [0,23] */ Ft.tm_mday = Rec.Day(); /* day of the month - [1,31] */ Ft.tm_mon = Rec.Month() - 1; /* months since January - [0,11] */ Ft.tm_year = Rec.Year() - 1900; /* years since 1900 */ Ft.tm_wday = Rec.DayOfWeek(); char Temp[64]; strftime(Temp, sizeof(Temp), "%a %b %d %H:%M:%S %Y", &Ft); s += Temp; s += "\r\n"; return s; } diff --git a/Code/Store3Common.h b/Code/Store3Common.h --- a/Code/Store3Common.h +++ b/Code/Store3Common.h @@ -1,286 +1,286 @@ #ifndef _STORE3_COMMON_H_ #define _STORE3_COMMON_H_ #include "lgi/common/Store3.h" #include "lgi/common/Mime.h" #include "ScribeDefs.h" class Store3MimeType { LAutoString Mt; public: Store3MimeType(const char *mt) { Mt.Reset(NewStr(mt)); } #undef IsText operator char*() { return Mt; } Store3MimeType &operator =(const char *s) { Mt.Reset(NewStr(s)); return *this; } bool Is(const char *Type) { return Mt && _stricmp(Mt, Type) == 0; } bool IsMultipart() { return Mt && _strnicmp(Mt, "multipart/", 10) == 0; } bool IsText() { return Mt && _strnicmp(Mt, "text/", 5) == 0; } bool IsPlainText() { return Is(sTextPlain); } bool IsHtml() { return Is(sTextHtml); } bool IsMixed() { return Is(sMultipartMixed); } bool IsAlternative() { return Is(sMultipartAlternative); } bool IsRelated() { return Is(sMultipartRelated); } }; class Store3Addr : public LDataPropI { LDataStoreI *Store; public: LString Name, Addr; int CC; Store3Addr(LDataStoreI *store, LDataPropI *i = NULL); ~Store3Addr(); Store3CopyDecl; void SetStore(LDataStoreI *s); void Empty(); const char *GetStr(int id) override; Store3Status SetStr(int id, const char *str) override; int64 GetInt(int id) override; Store3Status SetInt(int id, int64 i) override; size_t Sizeof(); bool GetVariant(const char *n, LVariant &Value, const char *Array = NULL) override; bool SetVariant(const char *n, LVariant &Value, const char *Array = NULL) override; }; class Store3Field : public LDataPropI { public: int32 Id, Width; Store3Field(LDataStoreI *Store, int id = 0, int width = 100); LDataPropI &operator =(LDataPropI &p) { LAssert(0); return *this; } const char *GetStr(int id); int64 GetInt(int id); Store3Status SetInt(int id, int64 i); }; template class Store3Attachment : public LDataI { protected: TStore *Kit = NULL; TMail *Mail = NULL; TAttach *Parent = NULL; DIterator Children; bool Dirty = true; LAutoPtr Import; void _Delete() { if (Parent) { #ifdef _DEBUG TAttach *This = dynamic_cast(this); #endif LAssert(This && Parent->Children.IndexOf(This) >= 0); Parent->Children.Delete(this); Parent = NULL; } else if (Mail) { Mail->Seg = NULL; } Mail = NULL; TAttach *c; while (Children.Length()) { c = Children.a[0]; LAssert(c->Parent == this); DeleteObj(c); } } public: Store3Attachment(TStore *store) { Kit = store; Children.State = Store3Loaded; } ~Store3Attachment() { // If this assert fires, you didn't call // _Delete() in your destructor. LAssert(Parent == NULL && Mail == NULL); } void Detach() { if (Parent) { TAttach *This = dynamic_cast(this); LAssert(This!=NULL); LAssert(Parent->Children.a.HasItem(This)); Parent->Children.a.Delete(This); Parent = NULL; Dirty = true; } else if (Mail) { if (Mail->Seg == this) { Mail->Seg = NULL; } Dirty = true; } Mail = NULL; } void AttachTo(TAttach *Obj) { Detach(); if (Obj) { TAttach *This = dynamic_cast(this); bool HasChild = Obj->Children.a.HasItem(This); bool HasParent = false; for (TAttach *p = This; p; p = p->GetParent()) { if (p == Obj) { HasParent = true; break; } } if (This && !HasChild && !HasParent) { Obj->Children.a.Add(This); Parent = Obj; SetMail(Obj->Mail); } else { LAssert(0); } } } void AttachTo(TMail *Obj) { Detach(); if (Obj) { if (Obj->Seg != NULL && Obj->Seg != this) { auto s = Obj->Seg; Obj->Seg->Detach(); delete s; } Obj->Seg = dynamic_cast(this); SetMail(Obj); } } void SetMail(TMail *m) { Mail = m; if (m) Kit = m->Store; // If the mail's store changes, we should update here // TAttach *This = dynamic_cast(this); for (unsigned i=0; iGetParent()) { if (p == c) { Ok = false; break; } } if (Ok) Children.a[i]->SetMail(m); } } TAttach *GetParent() { return Parent; } bool FindSegs(const char *SearchMimeType, LArray &Results) { const char *Mt = GetStr(FIELD_MIME_TYPE); if (!Mt) Mt = sTextPlain; if (!_stricmp(SearchMimeType, Mt)) { Results.Add(dynamic_cast(this)); } for (unsigned i=0; iFindSegs(SearchMimeType, Results); } return Results.Length() > 0; } - GDataIt GetList(int id) + LDataIt GetList(int id) { if (id == FIELD_MIME_SEG) return &Children; return 0; } TAttach *FindChildByMimeType(const char *MimeType) { for (unsigned i=0; i(Children[i]); if (a) { if (a->Is(MimeType)) return a; } } return NULL; } bool Is(const char *Type) { const char *Mt = GetStr(FIELD_MIME_TYPE); return Mt && _stricmp(Mt, Type) == 0; } bool IsMultipart() { const char *Mt = GetStr(FIELD_MIME_TYPE); return Mt && _strnicmp(Mt, "multipart/", 10) == 0; } bool IsPlainText() { return Is(sTextPlain); } bool IsHtml() { return Is(sTextHtml); } bool IsMixed() { return Is(sMultipartMixed); } bool IsAlternative() { return Is(sMultipartAlternative); } bool IsRelated() { return Is(sMultipartRelated); } virtual void OnSave() = 0; LDataStoreI *GetStore() { return Kit; } }; extern bool Store3ToGMime(LMime *Out, LDataPropI *In); extern bool GMimeToStore3(LDataPropI *Out, LMime *In, bool InMemOnly = false); extern LString HeadersFromStream(LStreamI *Msg); extern LString CreateMboxHeader(LDataI *Object); #endif diff --git a/Code/Store3Imap/ScribeImap.h b/Code/Store3Imap/ScribeImap.h --- a/Code/Store3Imap/ScribeImap.h +++ b/Code/Store3Imap/ScribeImap.h @@ -1,583 +1,583 @@ /** * \file * \author Matthew Allen * * http://tools.ietf.org/html/rfc3501 * http://tools.ietf.org/html/rfc4315 * * cmake -G "Visual Studio 14 2015 Win64" -Dprotobuf_BUILD_TESTS=OFF -DCMAKE_INSTALL_PREFIX=../../../../install ../.. */ #ifndef __SCRIBE_IMAP_H__ #define __SCRIBE_IMAP_H__ #include "lgi/common/Mime.h" #include "lgi/common/Mail.h" #include "Store3Common.h" #include "ScribeUtils.h" #define CastFld(obj) (dynamic_cast(obj)) #define CastMail(obj) (dynamic_cast(obj)) #define IMAP_PROTOBUF 0 #if IMAP_PROTOBUF #include "include/Scribe.pb.h" #else // Folder meta sub-element names #define TAG_FOLDER "Folder" #define TAG_MAIL "Mail" #define TAG_EMAILS "Emails" #define TAG_FIELDS "Fields" // Email attributes #define ATTR_UID "Uid" #define ATTR_SIZE "Size" #define ATTR_STRUCT "Structure" #define ATTR_FLAGS "Flags" // Flags sent to the server... #define ATTR_LOCAL "Local" // Flags not supported by IMAP... (not sent to the server) #define ATTR_DATE "Date" #define ATTR_COLOUR "Colour" #define ATTR_MSGID "MsgId" #define ATTR_SUBJECT "Subject" #define ATTR_FROM "From" #define ATTR_REPLYTO "ReplyTo" #define ATTR_LABEL "Label" #endif #define TIMEOUT_STORE_ONPULSE 3000 #define TIMEOUT_LISTING_CHUNK 1000 #define TIMEOUT_FOLDER_LOAD 100 // ms #define IMAP_BLOCK_SIZE 100 // chuck large processes into this many items enum ImapMsgType { IMAP_NULL, // Connection IMAP_ONLINE, IMAP_OFFLINE, IMAP_ERROR, // Lifespan events IMAP_ON_NEW, IMAP_ON_DEL, IMAP_ON_MOVE, // Folder IMAP_SELECT_FOLDER, IMAP_CREATE_FOLDER, IMAP_DELETE_FOLDER, IMAP_EXPUNGE_FOLDER, IMAP_RENAME_FOLDER, IMAP_LOAD_FOLDER, // Mail IMAP_FOLDER_SELECTED, IMAP_FOLDER_LISTING, IMAP_DOWNLOAD, IMAP_APPEND, IMAP_MOVE_EMAIL, // General IMAP_DELETE, IMAP_UNDELETE, IMAP_SET_FLAGS, // Check IMAP_MSG_MAX }; extern const char *ImapMsgTypeNames[IMAP_MSG_MAX]; extern bool ValidateImapDate(LDateTime &dt); /// MAPI folder info struct struct ImapFolderInfo { // The local path name LString Local; // The remote path name LString Remote; // [Optional] Highest known UID uint32_t LastUid; // Separator for IMAP path char Sep; ImapFolderInfo() { LastUid = -1; Sep = 0; } ImapFolderInfo &operator =(ImapFolderInfo &i) { Local = i.Local; Remote = i.Remote; return *this; } }; /// MAPI mail info struct, all of these fields are optional struct ImapMailInfo { // The email meta flags ImapMailFlags Flags; // The sequence number of the email uint32_t Seq; // The server UID of the email uint32_t Uid; // The initial headers LString Headers; // The local path to that .eml file LString Local; // The size of the email in bytes int64 Size; // The structure of the email LString Structure; // The internal date of the email LString Date; }; struct ImapMsg { ImapMsgType Type; // The path of the parent folder if required LString Parent; // A new remote path name (for moving/renaming folder) LString NewRemote; // Folder related... LArray Fld; // Mail related LArray Mail; uint8_t Last : 1; uint8_t New : 1; // Debug const char *File; int Line; int IntParam; LError Error; ImapMsg(ImapMsgType t, const char *file, int line) { Type = t; Last = 0; New = 0; File = file; Line = line; IntParam = 0; } void SetType(ImapMsgType t, const char *file, int line) { Type = t; File = file; Line = line; } }; extern char *TrimWhite(char *s); class ImapFolder; class ImapThread; class ImapMail; class ImapStore : public LDataStoreI { friend class ImapThread; friend class ImapFolder; friend class FolderLoaderThread; LVariant Host, User, Pass; int Port; int ConnectFlags; LDataEventsI *Callback; ImapFolder *Root; char *Cache; int AccountId; ImapThread *Thread; LStream *Log; ScribeAccountletStatusIcon Online; LArray Listing; int64 LastPulse; int64 SelectTime; int64 ListingTime; LArray Msgs; LAutoString ErrorMsg; LAutoPtr FolderLoader; MailProtocolProgress *ItemProgress, *DataProgress; LAutoPtr SettingStore; ImapFolder *GetParent(char *Local, char *Remote); ImapFolder *GetTrash(); ImapFolder *GetSystemFolder(int Type); public: ImapStore( char *host, int port, char *user, char *pass, int flags, LDataEventsI *callback, LCapabilityClient *caps, MailProtocolProgress *prog[2], LStream *log, int accoundid, LAutoPtr store); ~ImapStore(); void PostStore(ImapMsg *m) { Callback->Post(this, m); } bool PostThread(ImapMsg *m, bool UiPriority); void OnNew(const char *File, int Line, LDataFolderI *parent, LArray &new_items, int pos, bool is_new); bool OnDelete(const char *File, int Line, LDataFolderI *parent, LArray &del); bool OnChange(const char *File, int Line, LArray &items, int FieldHint); bool OnIdle(); LStreamI *GetLogger() { return Log; } LDataEventsI *GetEvents() { return Callback; } char *GetCache(); LStream *GetLog() { return Log; } uint64 Size(); LDataI *Create(int Type); LDataFolderI *GetRoot(bool create); Store3Status Move(LDataFolderI *NewFolder, LArray &Items); Store3Status Delete(LArray &Items, bool ToTrash); Store3Status Change(LArray &Items, int PropId, LVariant &Value, LOperator Operator); void Compact(LViewI *Parent, LDataPropI *Props, std::function OnStatus); void OnEvent(void *Param); const char *GetStr(int id); int64 GetInt(int id); Store3Status SetInt(int id, int64 val); LDataPropI *GetObj(int id); }; class ImapThread : public LThread, public LMutex { struct ImapThreadPrivate *d; void Log(LSocketI::SocketMsgType Type, const char *Fmt, ...); public: ImapThread(ImapStore *s, LCapabilityClient *caps, LStream *log, ProtocolSettingStore *Store); ~ImapThread(); void PostThread(ImapMsg *m, bool UiPriority); void PostStore(ImapMsg *m); int Main(); ImapMsg *GetListing(); void FlushListing(bool Force = false); }; class ImapAttachment : public Store3Attachment { LAutoString Name, MimeType, ContentId, Charset; LMime *Seg; void _ClearChildSegs(); /// This is set when the segment is not to be stored on disk. /// When signed and/or encrypted messages are stored, the original /// rfc822 image is maintained by not MIME decoding into separate /// segments but leaving it MIME encoded in one seg (headers and body). /// At runtime the segment is loaded and parsed into a temporary tree /// of LMail3Attachment objects. This flag is set for those temporary /// nodes. bool InMemoryOnly; public: ImapAttachment(ImapStore *store, ImapMail *mail = 0, LMime *seg = 0); ImapAttachment(ImapStore *store, ImapMail *mail, LDataPropI *att); ~ImapAttachment(); void SetInMemoryOnly(bool b); Store3CopyDecl; bool IsOrphan() override { return Mail == 0; } LMime *GetSeg() { return Seg; } const char *GetStr(int id) override; Store3Status SetStr(int id, const char *str) override; int64 GetInt(int id) override; Store3Status SetInt(int id, int64 i) override; uint32_t Type() override; bool IsOnDisk() override; uint64 Size() override; Store3Status Save(LDataI *Folder = 0) override; Store3Status Delete(bool ToTrash = false) override; LAutoStreamI GetStream(const char *file, int line) override; bool SetStream(LAutoStreamI stream) override; void OnSave() override; }; class ImapFolderData; class ImapMail : public LDataI { public: enum ImapMailState { ImapMailIdle, ImapMailGettingBody, ImapMailMoving, ImapMailDeleting }; protected: ImapMailState State; LAutoString TextCache, HtmlCache; LAutoString HeaderCache; LString UidCache; Store3Addr *ProcessAddress(Store3Addr &Addr, const char *FieldId, const char *RfcField); public: const char *AllocFile = NULL; int AllocLine = 0; #if IMAP_PROTOBUF typedef scribe::Email *IMeta; #else typedef LXmlTag *IMeta; #endif ImapStore *Store = NULL; ImapFolderData *Data = NULL; ImapFolder *Parent = NULL; Store3State Loaded = Store3Unloaded; LString Path; uint32_t Uid; // Server UID ImapMailFlags RemoteFlags; int LocalFlags; int Priority; int64 DataSize; uint8_t SegDirty : 1; LString MsgId; LString Subject; LString Label; LString Structure; Store3Addr From; Store3Addr Reply; LDateTime DateReceived; LDateTime DateSent; LColour Colour; DIterator To; LAutoStreamI Stream; ImapAttachment *Seg; ImapMail(ImapStore *store, const char *file = NULL, int line = 0, uint32_t uid = 0); ~ImapMail(); void SetState(ImapMailState s); void SetRemoteFlags(ImapMailFlags f); bool IsOrphan() override { return Parent == 0; } uint32_t Type() override { return MAGIC_MAIL; } bool IsOnDisk() override { return Path.Get() != NULL; } uint64 Size() override { return LFileSize(Path); } LDataStoreI *GetStore() override { return Store; } void Serialize(IMeta m, bool Write); IMeta GetMeta(bool AllowCreate = true); Store3CopyDecl; void Load(); void ReadMime(ImapMail::IMeta MetaTag); Store3Status SetRfc822(LStreamI *m) override; bool FindSegs(const char *Type, LArray &Segs); const char *GetStr(int id) override; Store3Status SetStr(int id, const char *str) override; int64 GetInt(int id) override; Store3Status SetInt(int id, int64 i) override; const LDateTime *GetDate(int id) override; Store3Status SetDate(int id, const LDateTime *i) override; LDataPropI *GetObj(int id) override; Store3Status SetObj(int id, LDataPropI *i) override; - GDataIt GetList(int id) override; + LDataIt GetList(int id) override; Store3Status Save(LDataI *Folder = 0) override; LAutoStreamI GetStream(const char *file, int line) override; bool SetStream(LAutoStreamI stream) override; /// This is called to start the deletion process Store3Status Delete(bool ToTrash) override; /// When the thread has deleted the email, this is called to finialize the deletion. bool OnDelete(); void OnDownload(LAutoString &Headers); }; #if IMAP_PROTOBUF #define FOLDER_META_NAME "Folder.proto" #else #define FOLDER_META_NAME "Folder.xml" #define PROP_SORT "Sort" #define PROP_EXPANDED "Expanded" #define PROP_UNREAD "Unread" #define PROP_READ "ReadPerm" #define PROP_WRITE "WritePerm" #define PROP_THREAD "Thread" #endif class ImapFolderFld : public LXmlTag, public LDataPropI { ImapFolder *Parent; public: ImapFolderFld(LDataStoreI *s, ImapFolder *p = 0, int id = 0, int width = 100); const char *GetStr(int id); int64 GetInt(int id); Store3Status SetInt(int id, int64 i); }; class ImapFolderData { friend class ImapStore; protected: // Mail containers, "UidMap" and "Mail" should always mirror each other 1:1 for email // that is not deleted. Deleted email will exist on in "UidMap" till they get expunged. // // The only exception to this is when a new email is being appended to the mailbox, // In that case it'll exist (with a temporary filename and no UID) in the Mail store // for a little while until the thread responds with an IMAP_APPEND msg. LHashTbl,ImapMail::IMeta> UidMap; DIterator Mail; void Swap(ImapFolderData &s) { UidMap.Swap(s.UidMap); Mail.Swap(s.Mail); } public: ImapMail::IMeta GetMeta(uint32_t Uid, bool Create = false); ImapMail *GetMail(uint32_t Uid); bool DeleteMeta(uint32_t Uid); bool AddMail(ImapMail *m, LArray *OnNew); bool DelMail(ImapMail *m); bool HasMail(ImapMail *m) { return Mail.IndexOf(m) >= 0; } virtual void LoadMail() = 0; virtual void SetDirty(bool b = true) = 0; }; class ImapFolder : public LDataFolderI, public ImapFolderData { friend struct ImapFolderLoadThread; int64 LastIdleSelect; bool IsOnline; int SortCache; LString RootName; LAutoPtr WriteThread; ImapFolder *_Parent; // Threaded loading... LAutoPtr LoadThread; #if IMAP_PROTOBUF bool ThreadView; ScribePerm PermRead; ScribePerm PermWrite; int SortField; bool Expanded; int UnreadCount; #else LXmlTag Meta; #endif // Loading of the files and folder int ScanState; LArray Folders; LArray Files; bool Dirty; // Events... LArray> OnLoad; public: ImapStore *Store; Store3SystemFolder System; /// The item type... int ItemType; /// The IMAP path LString Remote; /// The on disk cache folder LString Local; /// Temp storage for leaf name, used before written to disk LString LeafName; /// The sub-folders of this folder DIterator Sub; /// The configured fields in the folder DIterator Field; ImapFolder(ImapStore *store, const char *path = 0); ~ImapFolder(); Store3CopyDecl; bool IsOrphan() override { return _Parent == NULL; } bool IsRoot() { return ItemType == MAGIC_NONE; } uint32_t Type() override { return MAGIC_FOLDER; } bool IsOnDisk() override { return Local && Remote; } LDataStoreI *GetStore() override { return Store; } ImapFolder *Find(const char *Local, const char *Remote); int GetLastUid(); void Swap(ImapFolder &f); ImapFolder *GetParent() { return _Parent; } void SetParent(ImapFolder *p); void LoadMail() override; bool IsLoading() { return LoadThread.Get() != NULL; } void OnLoadMail(bool Threaded); Store3Status WhenLoaded(std::function cb); void LoadSub(bool All = false); void ScanFolder(); Store3Status Move(LArray &Items); LString MakeImapPath(char *From); uint64 Size() override; Store3Status Save(LDataI *Into = 0) override; Store3Status Delete(bool ToTrash = true) override; LAutoStreamI GetStream(const char *file, int line) override; bool SetStream(LAutoStreamI stream) override; const char *GetStr(int id) override; Store3Status SetStr(int id, const char *str) override; int64 GetInt(int id) override; Store3Status SetInt(int id, int64 i) override; const LDateTime *GetDate(int id) override; Store3Status SetDate(int id, const LDateTime *i) override; LDataPropI *GetObj(int id) override; - GDataIt GetList(int id) override; + LDataIt GetList(int id) override; LDataIterator &SubFolders() override; LDataIterator &Children() override; LDataIterator &Fields() override; Store3Status FreeChildren() override; Store3Status DeleteAllChildren() override; void SetDirty(bool b = true) override; LString MailPath(uint32_t Uid, bool CheckExists = true); // Mail container stuff ImapMail *FindByFile(char *File); bool Serialize(bool Write); // Events void OnPulse(); void OnDeleteComplete(); void OnListing(ImapMsg *m); void OnSelect(bool b) override; void OnCommand(const char *Name) override; void OnExpunge(); bool OnRename(const char *NewRemote); }; #endif diff --git a/Code/Store3Imap/ScribeImap_Folder.cpp b/Code/Store3Imap/ScribeImap_Folder.cpp --- a/Code/Store3Imap/ScribeImap_Folder.cpp +++ b/Code/Store3Imap/ScribeImap_Folder.cpp @@ -1,2584 +1,2584 @@ #include "lgi/common/Lgi.h" #include "lgi/common/NetTools.h" #include "ScribeImap.h" #include "ScribeUtils.h" #include "DomType.h" #if IMAP_PROTOBUF #include #endif #ifdef _DEBUG #define DEBUG_RENAME_OLD_XML 1 #else #define DEBUG_RENAME_OLD_XML 0 #endif #define TIMEOUT_IDLE_STATUS (60 * 1000) #define INVALID_CACHE -1000 ImapFolder::ImapFolder(ImapStore *store, const char *path) #if !IMAP_PROTOBUF : Meta(TAG_FOLDER) #endif { System = Store3SystemNone; Dirty = false; SortCache = INVALID_CACHE; IsOnline = false; LastIdleSelect = LCurrentTime(); _Parent = NULL; Store = store; ScanState = 0; Remote = path; #if IMAP_PROTOBUF ThreadView = false; PermRead = PermRequireNone; PermWrite = PermRequireNone; SortField = 0; Expanded = false; UnreadCount = 0; #endif ItemType = MAGIC_MAIL; if (Remote) { if (!_stricmp(Remote, "/")) ItemType = MAGIC_NONE; else if (!_stricmp(Remote, "/Trash") || !_stricmp(Remote, "/Deleted Items") || !_stricmp(Remote, "/Deleted Messages")) { ItemType = MAGIC_ANY; System = Store3SystemTrash; } char s[MAX_PATH_LEN] = ""; LMakePath(s, sizeof(s), Store->GetCache(), Remote); Local = s; char *Last = strrchr(Remote, '/'); if (Last++) { if (!_stricmp(Last, "Inbox")) System = Store3SystemInbox; else if (!_stricmp(Last, "Sent")) System = Store3SystemSent; else if (!_stricmp(Last, "Outbox")) System = Store3SystemOutbox; } Serialize(false); } } ImapFolder::~ImapFolder() { LoadThread.Reset(); Sub.DeleteObjects(); Field.DeleteObjects(); Mail.DeleteObjects(); // UidMap.DeleteObjects(); } void ImapFolder::SetParent(ImapFolder *p) { if (_Parent) { if (_Parent->Sub.State != Store3Loaded) LgiTrace("%s:%i - %p->Parent(%p) Sub not loaded :(\n", _FL, this, _Parent); else if (_Parent->Sub.IndexOf(this) < 0) LgiTrace("%s:%i - Folder(%p) not in parent(%p) Sub :(\n", _FL, this, _Parent); else _Parent->Sub.Delete(this); } _Parent = p; if (_Parent) { LAssert(_Parent->Sub.State != Store3Unloaded); _Parent->Sub.Insert(this, -1, true); } } void ImapFolder::SetDirty(bool b) { Dirty = b; if (Dirty && Mail.State != Store3Loaded) { LAssert(!"State needs to be loaded before we modify it."); } } struct ImapFolderWriterThread : public LThread { LString File; LString Data; ImapFolderWriterThread(const char *file, LString data) : LThread("WriterThread") { File = file; Data = data; Run(); } ~ImapFolderWriterThread() { uint64 Start = LCurrentTime(); while (!IsExited()) { LSleep(20); uint64 Now = LCurrentTime(); if (Now - Start > 1000) { LgiTrace("ImapFolder::WriterThread shutting down: %ims\n", (int)(Now-Start)); } } } int Main() { #if DEBUG_RENAME_OLD_XML if (LFileExists(File)) { // Don't overwrite... just rename auto parts = LString(File).RSplit(DIR_STR, 1); auto leaf = parts.Last().RSplit(".", 1); auto now = LDateTime::Now(); auto newleaf = leaf[0] + " " + now.Get().Replace(":","-").Replace("/","-") + "." + leaf[1]; auto dstFile = parts[0] + DIR_STR + newleaf; LError err; if (!FileDev->Move(File, dstFile, &err)) LgiTrace("%s:%i - Failed to rename '%s' -> '%s': %s\n", _FL, File.Get(), dstFile.Get(), err.GetMsg().Get()); } #endif LFile f; if (f.Open(File, O_WRITE)) { f.SetSize(0); ssize_t Wr = f.Write(Data, Data.Length()); f.Close(); if (Wr != Data.Length()) { LAssert(!"Failed to write all the XML"); LgiTrace("%s:%i - ImapFolder::WriterThread wrote %i of %i\n", _FL, (int)Wr, (int)Data.Length()); } } return 0; } }; bool ImapFolder::Serialize(bool Write) { bool Debug = false; // System == Store3SystemInbox; if (!ValidStr(Local)) return false; LFile::Path XmlPath(Local); XmlPath += FOLDER_META_NAME; LXmlTree t; LFile f; #if IMAP_PROTOBUF bool Status = false; if (Write) { LProfile p("ImapFolderWrite"); scribe::Folder folder; p.Add("Meta"); folder.set_readperm(PermRead); folder.set_writeperm(PermWrite); folder.set_sortfield(SortField); folder.set_itemtype(ItemType); folder.set_expanded(Expanded); folder.set_threadview(ThreadView); p.Add("Fields"); for (auto in: Field.a) { auto out = folder.add_fields(); out->set_id(in->GetInt(FIELD_ID)); out->set_width(in->GetInt(FIELD_WIDTH)); } p.Add("Email"); for (auto in: Mail.a) { try { #if 1 auto m = in->GetMeta(); if (m) { LAssert(m->IsInitialized()); folder.mutable_mail()->Add(std::move(*m)); } #else auto out = folder.add_mail(); if (in->Uid) out->set_uid(in->Uid); out->set_remoteflags(in->RemoteFlags.All); out->set_localflags(in->LocalFlags); if (in->DateSent.IsValid()) out->set_datesent(in->DateSent.Ts()); if (in->DateReceived.IsValid()) out->set_datereceived(in->DateReceived.Ts()); if (in->Label) out->set_label(in->Label); if (in->Subject) out->set_subject(in->Subject); out->set_size(in->DataSize); if (in->Path) out->set_filename(in->Path); out->set_priority(in->Priority); if (in->Structure) out->set_structure(in->Structure); if (in->From.Addr || in->From.Name) { auto a = new scribe::Address(); if (in->From.Addr) a->set_email(in->From.Addr.Get()); if (in->From.Name) a->set_name(in->From.Name.Get()); out->set_allocated_from(a); } for (auto to: in->To.a) { if (to->Addr || to->Name) { auto a = out->add_to(); if (to->Addr) a->set_email(to->Addr.Get()); if (to->Name) a->set_name(to->Name.Get()); } } if (in->Colour.IsValid()) out->set_colour(in->Colour.c32()); LAssert(out->IsInitialized()); #endif } catch (...) { LAssert(0); } } p.Add("FileWrite"); { LAssert(folder.IsInitialized()); std::ofstream file(s, std::ios_base::binary); if (Status = folder.SerializeToOstream(&file)) Dirty = false; } #if 0 { scribe::Folder f; std::ifstream file(s, std::ios_base::binary); bool r = f.ParseFromIstream(&file); LAssert(r); } #endif } else { Field.DeleteObjects(); Mail.DeleteObjects(); scribe::Folder folder; std::ifstream file(s, std::ios_base::binary); if (folder.ParseFromIstream(&file)) { // Meta PermRead = (ScribePerm)folder.readperm(); PermWrite = (ScribePerm)folder.writeperm(); SortField = folder.sortfield(); ItemType = folder.itemtype(); Expanded = folder.expanded(); ThreadView = folder.threadview(); // Fields: for (int i=0; iRemoteFlags.All = in.remoteflags(); out->LocalFlags = in.localflags(); out->DateSent.Set(in.datesent()); out->DateReceived.Set(in.datereceived()); out->Label = in.label().c_str(); out->Subject = in.subject().c_str(); out->DataSize = in.size(); out->Path = in.filename().c_str(); out->Priority = in.priority(); out->Structure = in.structure().c_str(); out->Parent = this; Mail.Insert(out, -1, true); // if (out->Uid) UidMap.Add(out->Uid, in); } } if (Mail.a.Length()) Mail.State = Store3Loaded; Status = true; } if (Field.Length() == 0) { // Set the default fields. Field.State = Store3Loaded; Field.Insert(new ImapFolderFld(GetStore(), this, FIELD_PRIORITY, 10)); Field.Insert(new ImapFolderFld(GetStore(), this, FIELD_FLAGS, 10)); Field.Insert(new ImapFolderFld(GetStore(), this, FIELD_FROM, 120)); Field.Insert(new ImapFolderFld(GetStore(), this, FIELD_SUBJECT, 200)); Field.Insert(new ImapFolderFld(GetStore(), this, FIELD_SIZE, 46)); Field.Insert(new ImapFolderFld(GetStore(), this, FIELD_DATE_SENT, 120)); #if 0 // def _DEBUG Field.Insert(new ImapFolderFld(GetStore(), this, FIELD_CACHE_FLAGS, 100)); Field.Insert(new ImapFolderFld(GetStore(), this, FIELD_CACHE_FILENAME, 140)); #endif Field.State = Store3Loaded; SetInt(FIELD_SORT, -6); SetDirty(); } } return Status; #else if (Write) { auto e = Meta.GetChildTag(TAG_FIELDS, true); uint64 t1; if (e) { e->EmptyChildren(); t1 = LCurrentTime(); for (unsigned i=0; iInsertTag(new LXmlTag(*f1)); } } else { LAssert(0); return false; } if (Mail.State != Store3Loaded) { LAssert(!"Not loaded? We can't overwrite the valid XML with invalid..."); return false; } if ((e = Meta.GetChildTag(TAG_EMAILS, true))) { for (auto it : UidMap) e->InsertTag(it.value); if (Debug) LgiTrace("%s:%i - Meta has " LPrintfSizeT " email.\n", _FL, e->Children.Length()); } LStringPipe Buf(16<<10); if (!t.Write(&Meta, &Buf)) { LAssert(0); return false; } if (Debug) LgiTrace("%s:%i - Buf has %s.\n", _FL, LFormatSize(Buf.GetSize()).Get()); auto Xml = Buf.NewGStr(); if (WriteThread.Reset(new ImapFolderWriterThread(XmlPath, Xml))) { if (e) e->Children.Empty(); Dirty = false; } else { LAssert(0); return false; } } else // read { Field.DeleteObjects(); if (LFileExists(XmlPath)) { LFile f; if (f.Open(XmlPath, O_READ) && t.Read(&Meta, &f, 0)) { if (Debug) LgiTrace("%s:%i - In file has %s.\n", _FL, LFormatSize(f.GetSize()).Get()); // Fields LXmlTag *t = Meta.GetChildTag(TAG_FIELDS); if (t && t->Children.Length() > 0) { for (auto c: t->Children) { int Id = c->GetAsInt("Id"); int Width = c->GetAsInt("Width"); ImapFolderFld *ff = new ImapFolderFld(GetStore(), this, Id, Width); if (ff) { Field.Insert(ff, -1, true); } } } Field.State = Store3Loaded; // Email if ((t = Meta.GetChildTag(TAG_EMAILS))) { // Move all the tags into the Sid hash table for (auto c: t->Children) { auto Uid = c->GetAsInt(ATTR_UID); if (!Uid) { LAssert(!"Not a valid email tag."); DeleteObj(c); } else { UidMap.Add(Uid, c); } } if (Debug) LgiTrace("%s:%i - Meta read " LPrintfSizeT " email.\n", _FL, UidMap.Length()); t->Children.Empty(); } } } if (Field.Length() == 0) { // Set the default fields. Field.State = Store3Loaded; Field.Insert(new ImapFolderFld(GetStore(), this, FIELD_PRIORITY, 10)); Field.Insert(new ImapFolderFld(GetStore(), this, FIELD_FLAGS, 10)); Field.Insert(new ImapFolderFld(GetStore(), this, FIELD_FROM, 120)); Field.Insert(new ImapFolderFld(GetStore(), this, FIELD_SUBJECT, 200)); Field.Insert(new ImapFolderFld(GetStore(), this, FIELD_SIZE, 46)); Field.Insert(new ImapFolderFld(GetStore(), this, FIELD_DATE_SENT, 120)); #if 0 // def _DEBUG Field.Insert(new ImapFolderFld(GetStore(), this, FIELD_CACHE_FLAGS, 100)); Field.Insert(new ImapFolderFld(GetStore(), this, FIELD_CACHE_FILENAME, 140)); #endif SetInt(FIELD_SORT, -6); SetDirty(); } } return true; #endif } Store3Status ImapFolder::Move(LArray &Items) { Store3Status Status = Store3Error; LHashTbl, bool> Has; LArray Moved; LAutoPtr Mv(new ImapMsg(IMAP_MOVE_EMAIL, _FL)); for (unsigned n=0; nType() == MAGIC_MAIL) { ImapMail *m = CastMail(Items[n]); if (m) { if (Has.Find(m->Uid)) { LAssert(!"Duplicate UID."); continue; } Has.Add(m->Uid, true); if (m->Parent) { if (!m->RemoteFlags.ImapDeleted) { if (!Mv->Parent) Mv->Parent = m->Parent->Remote.Get(); if (_stricmp(Mv->Parent, m->Parent->Remote) || Mv->Mail.Length() >= IMAP_BLOCK_SIZE) { Store->PostThread(Mv.Release(), false); Mv.Reset(new ImapMsg(IMAP_MOVE_EMAIL, _FL)); Mv->Parent = m->Parent->Remote.Get(); } ImapMailInfo &Inf = Mv->Mail.New(); Inf.Uid = m->Uid; if (!Mv->NewRemote) Mv->NewRemote = Remote.Get(); m->RemoteFlags.ImapDeleted = true; // It'll be set soon anyway... m->SetState(ImapMail::ImapMailMoving); Moved.Add(m); Status = Store3Delayed; } else LgiTrace("%s:%i - Can't move deleted email '%i'\n", _FL, m->Uid); } else LAssert(!"No parent."); } else LAssert(!"Not an imap mail."); } else if (Items[n]->Type() == MAGIC_FOLDER) { /* ImapFolder *f = dynamic_cast(Items[n]); if (f) { if (f->Parent) { char p[MAX_PATH_LEN], *Leaf = strrchr(Local, DIR_CHAR); if (Leaf) { Leaf++; LMakePath(p, sizeof(p), Local, Leaf); if (FileDev->Move(f->Remote, p)) { // Adjust path f->Remote.Reset(NewStr(p)); // Adjust containers and pointers LAssert(f->Parent->Sub.IndexOf(f) >= 0); f->Parent->Sub.Delete(f); f->Parent = this; LAssert(Sub.IndexOf(f) < 0); Sub.Insert(f); Status = true; // Adjust all the path's of the email in the ImapFolder for (int i=0; iMail.Length(); i++) { ImapMail *m = dynamic_cast(f->Mail[i]); if (m) { char *MailLeaf = strchr(m->Path, DIR_CHAR); if (MailLeaf) { char mpath[MAX_PATH_LEN]; LMakePath(mpath, sizeof(mpath), f->Remote, MailLeaf+1); m->Path.Reset(NewStr(mpath)); } else LAssert(!"Not a valid path."); } } Status = true; } } else LAssert(!"Not a valid path."); } else LAssert(!"No parent."); } else LAssert(!"Not a valid imap folder."); */ } else LAssert(!"Not a valid object type."); } if (Mv->Parent && Mv->Mail.Length()) { Store->PostThread(Mv.Release(), false); Status = Store3Delayed; } /* This should be handled by the OnNew callbacks if (NewUnread) { SetInt(FIELD_UNREAD, GetInt(FIELD_UNREAD) + NewUnread); LArray a; a.Add(this); Store->OnChange(_FL, a, FIELD_UNREAD); } */ if (Moved.Length()) { // This allows the UI to display them as "Moving..." Store->OnChange(_FL, Moved, 0); } return Status; } /* Store3CopyImpl(ImapFolder) { ImapFolder *f = CastFld(&p); if (f) { SetStr(FIELD_FOLDER_NAME, f->GetStr(FIELD_FOLDER_NAME)); SetInt(FIELD_FOLDER_THREAD, f->GetInt(FIELD_FOLDER_THREAD)); SetInt(FIELD_FOLDER_PERM_READ, f->GetInt(FIELD_FOLDER_PERM_READ)); SetInt(FIELD_FOLDER_PERM_WRITE, f->GetInt(FIELD_FOLDER_PERM_WRITE)); SetInt(FIELD_SORT, f->GetInt(FIELD_SORT)); SetInt(FIELD_FOLDER_OPEN, f->GetInt(FIELD_FOLDER_OPEN)); SetInt(FIELD_UNREAD, f->GetInt(FIELD_UNREAD)); SetInt(FIELD_FOLDER_TYPE, f->GetInt(FIELD_FOLDER_TYPE)); } return true; } */ Store3CopyImpl(ImapFolder) { SetStr(FIELD_FOLDER_NAME, p.GetStr(FIELD_FOLDER_NAME)); SetInt(FIELD_SORT, p.GetInt(FIELD_SORT)); SetInt(FIELD_FOLDER_TYPE, p.GetInt(FIELD_FOLDER_TYPE)); SetInt(FIELD_UNREAD, p.GetInt(FIELD_UNREAD)); SetInt(FIELD_FOLDER_INDEX, p.GetInt(FIELD_FOLDER_INDEX)); SetInt(FIELD_FOLDER_OPEN, p.GetInt(FIELD_FOLDER_OPEN)); SetInt(FIELD_FOLDER_THREAD, p.GetInt(FIELD_FOLDER_THREAD)); SetInt(FIELD_FOLDER_PERM_READ, p.GetInt(FIELD_FOLDER_PERM_READ)); SetInt(FIELD_FOLDER_PERM_WRITE, p.GetInt(FIELD_FOLDER_PERM_WRITE)); Field.DeleteObjects(); ImapFolder *Folder = dynamic_cast(&p); if (Folder) { LDataIterator &it = Folder->Fields(); Folder->Field.State = Store3Loaded; for (LDataPropI *f = it.First(); f; f = it.Next()) { Field.Insert(new ImapFolderFld( GetStore(), this, (int)f->GetInt(FIELD_ID), (int)f->GetInt(FIELD_WIDTH))); } } return true; } LString ImapFolder::MakeImapPath(char *From) { LString Ret; if (From) { size_t CacheLen = strlen(Store->GetCache()); size_t FromLen = strlen(From); if (CacheLen >= FromLen) { LAssert(!"From path too short"); } else { Ret = From + CacheLen; Ret = Ret.Replace("\\", "/"); } } return Ret; } uint64 ImapFolder::Size() { return 0; } void ImapFolder::ScanFolder() { if (ScanState == 0) { ScanState = 1; LDirectory d; for (int b = d.First(Local); b; b = d.Next()) { if (d.IsDir()) Folders.New() = d.GetName(); } } } void ImapFolder::LoadSub(bool All) { if (Sub.State == Store3Unloaded) { Sub.State = Store3Loading; // Look at the local folders... ScanFolder(); for (unsigned i=0; iSetParent(this); } else LgiTrace("%s:%i - alloc err\n", _FL); } else LgiTrace("%s:%i - '%s' can't make path\n", _FL, s); } if (All) Fld->LoadSub(true); } // uint64 End = LCurrentTime(); // LgiTrace("LoadSub '%s' Time: %i, Items: %i\n", Remote.Get(), (int)(End-Start), Items); Sub.State = Store3Loaded; } } struct ImapFolderLoadThread : public LThread, public ImapFolderData { ImapFolder *f; LString Local, Remote; LArray PostDel; ImapFolderLoadThread(ImapFolder *folder) : f(folder), LThread("ImapFolderLoadThread") { Swap(*f); Local = f->Local.Get(); Remote = f->Remote.Get(); Run(); } void LoadMail() { } void SetDirty(bool b = true) { } int Main() { // auto StartTs = LCurrentTime(); // In same cases like an "OnNew" event mail can show up in a folder // before we load it. That is allowed but we have to de-duplicate it // here, so create a hash table of existing entries and don't create // new ImapMail objects for them in the lower loop LHashTbl, ImapMail*> Map; for (unsigned i=0; iUid != 0); if (m->Uid) Map.Add(m->Uid, m); } auto startTs = LCurrentTime(); ssize_t pos = 0; for (auto it: UidMap) { #if IMAP_PROTOBUF LAssert(0); #else auto ServerId = it.value->GetAsInt(ATTR_UID); if (ServerId && !it.value->GetContent() && Local) { char p[MAX_PATH_LEN], leaf[32]; sprintf_s(leaf, sizeof(leaf), "%i.eml", ServerId); LMakePath(p, sizeof(p), Local, leaf); // Files may not exist if they haven't downloaded yet... it.value->SetContent(leaf); } if (it.value->GetContent() && ServerId) { // Check the format of the msgid, old versions of Scribe used to // leave new-lines in the text... this fixes that on the fly. char *MsgId = it.value->GetAttr(ATTR_MSGID); if (MsgId && strchr(MsgId, '\n')) { LAutoString a(TrimStr(MsgId, " \t\r\n")); it.value->SetAttr(ATTR_MSGID, a); f->SetDirty(); } // Load an email from the cache file if it's not already in the // Mail array. Check there is no existing object: ImapMail *m = Map.Find(ServerId); if (!m) { // No? Well create one: m = new ImapMail(f->Store, _FL, ServerId); if (m) { m->Data = this; m->Parent = f; if (!AddMail(m, NULL)) { DeleteObj(m); } } else if (m->RemoteFlags.ImapDeleted) { PostDel.Add(m); } } } else LAssert(!"Invalid tag..."); #endif pos++; auto now = LCurrentTime(); if (now - startTs >= 500) { startTs = now; if (f->Mail.Prog) f->Mail.Prog(pos, UidMap.Length()); } } for (auto del: PostDel) DelMail(del); // LgiTrace("ImapFolderLoadThread::Main(%s): " LPrintfInt64 "ms.\n", // Remote.Get(), LCurrentTime() - StartTs); auto Store = f->GetStore(); LAutoPtr msg(new ImapMsg(IMAP_LOAD_FOLDER, _FL)); auto &fi = msg->Fld.New(); fi.Local = Local.Get(); Store->GetEvents()->Post(Store, msg.Release()); return 0; } }; void ImapFolder::LoadMail() { if (Mail.State != Store3Unloaded) return; // We really do not want to be creating a second load thread, while one is already running. LAssert(!LoadThread); LoadThread.Reset(new ImapFolderLoadThread(this)); // This happens after the thread creation, because the thread steals the state data // from the Mail container using a Swap. Mail.State = Store3Loading; // Wait and see if it loads quickly: auto StartTs = LCurrentTime(); while ( LCurrentTime() - StartTs < TIMEOUT_FOLDER_LOAD && !LoadThread->IsExited()) LSleep(1); // LgiTrace("ImapFolder::LoadMail(%s) threaded=%i\n", Remote.Get(), !LoadThread->IsExited()); if (LoadThread->IsExited()) { // Yes it did... finish the processing: OnLoadMail(false); } } void ImapFolder::OnLoadMail(bool Threaded) { if (Mail.State == Store3Loaded) return; if (LoadThread) { // auto StartTs = LCurrentTime(); // Return all the data to the main folder ImapFolderData::Swap(*LoadThread.Get()); // NULL out the data ptrs for (auto m: Mail.a) m->Data = NULL; LoadThread.Reset(); //LgiTrace("ImapFolder::OnLoadMail(%s) prcessing: " LPrintfInt64 "ms.\n", // Remote.Get(), LCurrentTime() - StartTs); } else LAssert(!"Where is the thread?"); if (_Parent) { ImapMsg *m = new ImapMsg(IMAP_FOLDER_LISTING, _FL); if (m) { ImapFolderInfo &i = m->Fld.New(); i.Remote = Remote.Get(); i.Local = Local.Get(); i.LastUid = GetLastUid(); Store->PostThread(m, false); } } // Set this collection to loaded even if we get updates from the // server later. We can update the UI through the OnNew/OnDelete // events. Mail.State = Store3Loaded; // Call any onload callbacks... for (auto &cb: OnLoad) cb(Store3Delayed); OnLoad.Empty(); if (Threaded) { // Tell the UI thread about this... it'll need to update the screen LArray Items; Items.Add(this); Store->OnChange(_FL, Items, FIELD_STATUS); } } Store3Status ImapFolder::WhenLoaded(std::function cb) { if (Mail.State == Store3Loaded) { cb(Store3Success); return Store3Success; } // Save the callback for later... OnLoad.Add(cb); if (Mail.State == Store3Unloaded) LoadMail(); // Start the loading process... return Store3Delayed; } Store3Status ImapFolder::Save(LDataI *Into) { if (!IsOnDisk() && Into) { if (!LeafName) { LAssert(!"No leaf name set yet."); return Store3Error; } ImapFolder *f = dynamic_cast(Into); if (f) { f->LoadSub(); char s[MAX_PATH_LEN]; LMakePath(s, sizeof(s), f->Local, LeafName); Local = s; Remote = MakeImapPath(Local); if (LDirExists(Local)) { LAssert(!"Sub-folder already exists."); Remote.Empty(); Local.Empty(); return Store3Error; } FileDev->CreateFolder(Local); ImapMsg *m = new ImapMsg(IMAP_CREATE_FOLDER, _FL); if (!m) return Store3Error; ImapFolderInfo &Inf = m->Fld.New(); Inf.Local = Local.Get(); Inf.Remote = Remote.Get(); Store->PostThread(m, false); SetParent(f); Field.State = Store3Loaded; } else { LAssert(!"Not a valid parent folder."); return Store3Error; } } if (Mail.State != Store3Loaded) { // First read the state off disk so we don't lose anything... LAssert(!"State needs to be loaded before saving."); return Store3Error; } return Serialize(true) ? Store3Success : Store3Error; } Store3Status ImapFolder::Delete(bool ToTrash) { ImapMsg *m = new ImapMsg(IMAP_DELETE_FOLDER, _FL); if (m) { ImapFolderInfo &Inf = m->Fld.New(); Inf.Local = Local.Get(); Inf.Remote = Remote.Get(); if (Store->PostThread(m, false)) return Store3Delayed; } return Store3Error; } class ImapDownload : public LStream { LVariant Path; LFile *f; public: ImapDownload(char *p) { Path = p; f = 0; } ~ImapDownload() { DeleteObj(f); } int Open(const char *Str, int Int) { LAssert(0); return false; } bool IsOpen() { return f ? f->IsOpen() : 0; } int Close() { LAssert(0); return false; } int64 GetSize() { return f ? f->GetSize() : -1; } int64 SetSize(int64 Size) { return f ? f->SetSize(Size) : -1; } int64 GetPos() { return f ? f->GetPos() : -1; } int64 SetPos(int64 Pos) { return f ? f->SetPos(Pos) : -1; } ssize_t Read(void *Buffer, ssize_t Size, int Flags = 0) { return f ? f->Read(Buffer, Size, Flags) : 0; } ssize_t Write(const void *Buffer, ssize_t Size, int Flags = 0) { if (!f) { if ((f = new LFile)) { if (!f->Open(Path.Str(), O_WRITE)) { LgiTrace("%s:%i - Error: Failed to open IMAP file '%s'\n", _FL, Path.Str()); } } } return f ? f->Write(Buffer, Size, Flags) : 0; } }; #if !IMAP_PROTOBUF int IdxCmp(LXmlTag *a, LXmlTag *b, int d) { char *aa = a->GetAttr(ATTR_UID); char *bb = b->GetAttr(ATTR_UID); LAssert(aa && bb); return atoi(aa) - atoi(bb); } #endif static const char *Ws = " \t\r\n"; #define Skip(s) while (*s && strchr(Ws, *s)) s++ char *ImapTok(char *&s) { Skip(s); if (*s == '\"' || *s == '\'') { char d = *s++; char *Start = s; char *End = s; for (; *End; End++) { if (*End == d) break; if (*End == '\\') End++; } if (!End) return 0; s = End + 1; return NewStr(Start, End - Start); } else if (*s == '{') { s++; char *e = strchr(s, '}'); if (e) { int Size = atoi(s); s = e + 1; Skip(s); char *Str = NewStr(s, Size); s += Size; return Str; } else { LAssert(!"Parse error"); return 0; } } else { char *Start = s; while (*s && !strchr(" \r\t\n)", *s)) s++; return NewStr(Start, s - Start); } } struct ImapPair { LAutoString Name; LAutoString Value; }; bool SkipStructure(char *&s) { int Depth = 0; while (s && *s) { if (*s == '(') { s++; Depth++; } else if (*s == ')') { if (Depth == 0) break; s++; Depth--; } else if (*s == '\"' || *s == '\'') { char delim = *s++; while (*s && *s != delim) s++; if (*s == delim) s++; } else s++; } return true; } bool ImapParseStructure(LXmlTag *t, char *&s) { LXmlTag *c = new LXmlTag("Seg"); if (!c) return false; t->InsertTag(c); char m[256]; while (s && *s && *s != ')') { // Check for nesting... Skip(s); if (*s == '(') { // Read nested segment(s) s++; if (!ImapParseStructure(c, s)) return false; Skip(s); if (*s == ')') { // Scan past the end marker s++; } else { LgiTrace("Error: Parsing IMAP structure - wrong token after sub\n"); return false; } } else // Read token stream that represents a MIME segment { LArray p; while (s && *s && *s != ')') { Skip(s); if (*s == '(') { // Named values s++; Skip(s); while (s && *s && *s != ')') { ImapPair &v = p.New(); v.Name.Reset(ImapTok(s)); Skip(s); if (*s == '(') { // Sublist of values... argh. // This might need to be recursive? // ssize_t DelIndex = (ssize_t)p.Length()-1; s++; Skip(s); while (s && *s && *s != ')') { ImapPair &vv = p.New(); LAutoString Name(ImapTok(s)); sprintf_s(m, sizeof(m), "%s(%s)", v.Name.Get(), Name.Get()); vv.Name.Reset(NewStr(m)); Skip(s); if (*s == ')') { LgiTrace("Error: Parsing IMAP structure - wrong token in named value\n"); return false; } vv.Value.Reset(ImapTok(s)); Skip(s); } if (*s == ')') { s++; } else { LgiTrace("Error: Parsing IMAP structure - wrong token after named value\n"); return false; } Skip(s); v.Name.Reset(); continue; } else if (*s == ')') { // Move the name to the value... a single token is always just a value. v.Value = v.Name; } else { v.Value.Reset(ImapTok(s)); } Skip(s); } if (*s == ')') { s++; } else { LgiTrace("Error: Parsing IMAP structure - wrong token after named value\n"); return false; } } else { // Unnamed value ImapPair &v = p.New(); v.Value.Reset(ImapTok(s)); if (p.Length() == 7 && !_stricmp(p[0].Value, "message")) { if (!SkipStructure(s)) return false; } } } unsigned i = 0; if (p.Length() > 1) { if (c->Children.Length()) { ImapPair &a = p[i++]; if (a.Value) { sprintf_s(m, sizeof(m), "multipart/%s", a.Value.Get()); if (!c->SetAttr("mimetype", StrLwr(m))) return false; } } else { ImapPair &a = p[i++]; ImapPair &b = p[i++]; if (a.Value && b.Value) { sprintf_s(m, sizeof(m), "%s/%s", a.Value.Get(), b.Value.Get()); if (!c->SetAttr("mimetype", StrLwr(m))) return false; } } } while (i < p.Length()) { ImapPair &v = p[i++]; if (v.Name) { if (_stricmp(v.Name, "boundary") && !strchr(v.Name, '*')) { if (!c->SetAttr(v.Name, v.Value)) return false; } } else if (v.Value) { if (i == 6) { if (!c->SetAttr("encoding", v.Value)) return false; } else if (i == 7) { if (!c->SetAttr("size", atoi(v.Value))) return false; } } } } } return true; } struct UidTbl : public LHashTbl,ImapMail*> { UidTbl(DIterator &Mail) { for (unsigned i=0; iUid) { Add(m->Uid, m); } else { // There is no UID in the case that there's a new message // being appended to the IMAP folder and hasn't got a UID // yet... just ignore it. Add the UID to the table when // the IMAP_APPEND message gets back from the worker thread. } } else LAssert(0); } } }; LString ImapFolder::MailPath(uint32_t Uid, bool CheckExists) { char f[32], p[MAX_PATH_LEN]; sprintf_s(f, sizeof(f), "%i.eml", Uid); LMakePath(p, sizeof(p), Local, f); if (CheckExists) { LAssert(!LFileExists(p)); } return p; } #define IMAP_ONLISTING_LOG 0 void ImapFolder::OnListing(ImapMsg *m) { // This code does a replication between the list of mail on the server and the local // database, removing local email that doesn't exist on the server, updating existing // mail and creating new mail for objects only on the server. LArray NewMail, OldMail, DelMail, ChangedMail; // Build a hash of our local mail... UidTbl Tbl(Mail); #if IMAP_ONLISTING_LOG LgiTrace("OnListing(%s)\n", Remote.Get()); #endif ImapFolderInfo &Fld = m->Fld[0]; bool Unclean = false; // Go through the incoming list of mail... for (auto &Inf: m->Mail) { if (!Inf.Uid) { LAssert(!"No UID."); continue; } // Do we have the mail in our local mail? ImapMail *Existing = Tbl.Find(Inf.Uid); if (Existing) { // Already exists. Tbl.Delete(Inf.Uid); // Clear the ref, we've "seen" this email on the server. // Update flags? auto ExistingMeta = UidMap.Find(Inf.Uid); if (!ExistingMeta) { LAssert(!"No meta for this email."); continue; } bool ItemDirty = false; #if IMAP_ONLISTING_LOG bool dTypes[3] = {0}; #endif if (Inf.Size > 0) { #if IMAP_PROTOBUF auto mSize = ExistingMeta->size(); if (mSize != Inf.Size) { ExistingMeta->set_size(Inf.Size); ItemDirty = Unclean = true; #if IMAP_ONLISTING_LOG dTypes[0] = true; #endif } #else char *sSize = ExistingMeta->GetAttr(ATTR_SIZE); if (!sSize || atoi64(sSize) != Inf.Size) { LString s; s = Inf.Size; Unclean |= ExistingMeta->SetAttr(ATTR_SIZE, s); ItemDirty = true; #if IMAP_ONLISTING_LOG dTypes[0] = true; #endif } #endif } #if IMAP_ONLISTING_LOG LString sDate; #endif if (Inf.Date) { LDateTime Dt; if (Dt.Decode(Inf.Date)) { #if IMAP_ONLISTING_LOG auto Tmp = Dt; #endif Dt.ToUtc(); #if IMAP_ONLISTING_LOG sDate.Printf("[%s, %s, %s, %s]", Inf.Date.Get(), Tmp.Get().Get(), Dt.Get().Get(), Existing->DateSent.Get().Get()); #endif if (Dt != Existing->DateSent) { ValidateImapDate(Dt); Existing->DateSent = Dt; #if IMAP_PROTOBUF ExistingMeta->set_datesent(Dt.Ts()); Unclean = true; #else char c[32]; Dt.SetFormat(GDTF_YEAR_MONTH_DAY); Dt.Get(c, sizeof(c)); Unclean |= ExistingMeta->SetAttr(ATTR_DATE, c); #endif ItemDirty = true; #if IMAP_ONLISTING_LOG dTypes[1] = true; #endif } } } #if IMAP_PROTOBUF ImapMailFlags CurFlags; CurFlags.All = ExistingMeta->remoteflags(); #else ImapMailFlags CurFlags(ExistingMeta->GetAttr(ATTR_FLAGS)); #endif if (CurFlags.All != Inf.Flags.All) { if (Inf.Flags.ImapDeleted) { // Becoming deleted... DelMail.Add(Existing); // But don't let it fall into the 'Changed' items.. ItemDirty = false; } else { // Flags changing.. #if IMAP_PROTOBUF ExistingMeta->set_remoteflags(Inf.Flags.All); #else Unclean |= ExistingMeta->SetAttr(ATTR_FLAGS, Inf.Flags.Get()); #endif ItemDirty = true; #if IMAP_ONLISTING_LOG dTypes[2] = true; #endif } } if (ItemDirty) { LAssert(!DelMail.HasItem(Existing)); ChangedMail.Add(Existing); #if IMAP_ONLISTING_LOG LgiTrace("%s:%i %i:%i existing changed %i,%i,%i %s\n", _FL, i, Inf.Uid, dTypes[0], dTypes[1], dTypes[2], sDate.Get()); #endif } } else if (!Inf.Flags.ImapDeleted) { // Doesn't exist, so must be new. ImapMail *New = new ImapMail(Store, _FL, Inf.Uid); if (New) { #if IMAP_ONLISTING_LOG LString sDate; #endif New->Path = MailPath(Inf.Uid, false); if (Inf.Date) { if (New->DateSent.Decode(Inf.Date)) { #if IMAP_ONLISTING_LOG auto Tmp = New->DateSent; #endif New->DateSent.ToUtc(); ValidateImapDate(New->DateSent); #if IMAP_ONLISTING_LOG sDate.Printf("[%s, %s, %s]", Inf.Date.Get(), Tmp.Get().Get(), New->DateSent.Get().Get()); #endif } else { LgiTrace("%s:%i - Date parse failed '%s'\n", _FL, Inf.Date.Get()); New->DateSent.Decode(Inf.Date); } } New->SetRemoteFlags(Inf.Flags); New->Parent = this; bool IsNewMail = /* The problem with using the recent flag for IsNew is that other mail clients can be reading the inbox causing the recent flag to be consumed. This means Scribe sees the unread mail as old and doesn't trigger the required filters. At least that's my understanding. New->RemoteFlags.ImapRecent && */ !New->RemoteFlags.ImapSeen && System == Store3SystemInbox; #if IMAP_ONLISTING_LOG LgiTrace("%s:%i - Imap.IsNewMail %i, %i, %i(%i) '%s'\n", _FL, New->RemoteFlags.ImapRecent, New->RemoteFlags.ImapSeen, System == Store3SystemInbox, System, Inf.Flags.Get()); #endif // auto meta = New->GetMeta(false); AddMail(New, IsNewMail ? &NewMail : &OldMail); auto t = New->GetMeta(); LAssert(t != NULL); #if IMAP_ONLISTING_LOG LgiTrace("%s:%i idx=%i uid=%i new mail %i %s\n", _FL, i, Inf.Uid, IsNewMail, sDate.Get()); #endif if (t) { if (Inf.Size > 0) { #if IMAP_PROTOBUF t->set_size(Inf.Size); Unclean = true; #else LString s; s = Inf.Size; Unclean |= t->SetAttr(ATTR_SIZE, s); #endif } char *Struct = Inf.Structure; #if !IMAP_PROTOBUF if (Struct && !ImapParseStructure(t, Struct)) { #ifdef _DEBUG LFile f; if (f.Open( #ifdef WIN32 "c:\\temp\\" #endif "imap-struct.txt", O_WRITE)) { f.SetSize(0); f.Write(Inf.Structure, strlen(Inf.Structure)); f.Close(); LAssert(!"Imap structure parse failed."); } #endif } #endif } } } } // Now 'Tbl' contains all the local mail NOT on the server... // for (ImapMail *Old = Tbl.First(); Old; Old = Tbl.Next()) for (auto it : Tbl) { auto OldUid = it.value->Uid; // Why is this condition here??? if (Fld.LastUid == 0 || // Ie we are doing a full folder refresh or initial load OldUid > Fld.LastUid) // Or we are updated all the email after Fld.LastUid { #if IMAP_ONLISTING_LOG LgiTrace("%s:%i uid:%i deleted (notify)\n", _FL, it.key); #endif DelMail.Add(it.value); } else { #if IMAP_ONLISTING_LOG LgiTrace("%s:%i uid:%i deleted (no-notify)\n", _FL, it.key); #endif } } // Process all the callback events if (OldMail.Length()) Store->OnNew(_FL, this, OldMail, -1, false); if (NewMail.Length()) Store->OnNew(_FL, this, NewMail, -1, true); if (DelMail.Length() && Store->OnDelete(_FL, this, DelMail)) { DelMail.DeleteObjects(); Unclean = true; } if (ChangedMail.Length()) Store->OnChange(_FL, ChangedMail, 0); if (m->Last) Mail.State = Store3Loaded; if (Unclean) SetDirty(); IsOnline = true; } void ImapFolder::Swap(ImapFolder &f) { LAssert(WriteThread.Get() == NULL); // Probably can't swap this. LSwap(LastIdleSelect, f.LastIdleSelect); LSwap(IsOnline, f.IsOnline); LSwap(ScanState, f.ScanState); LSwap(Dirty, f.Dirty); LSwap(Store, f.Store); LSwap(System, f.System); LSwap(ItemType, f.ItemType); auto p1 = GetParent(); SetParent(f.GetParent()); f.SetParent(p1); #if IMAP_PROTOBUF LSwap(ThreadView, f.ThreadView); LSwap(PermRead, f.PermRead); LSwap(PermWrite, f.PermWrite); LSwap(SortField, f.SortField); LSwap(Expanded, f.Expanded); LSwap(UnreadCount, f.UnreadCount); #else Meta.Swap(f.Meta); #endif RootName.Swap(f.RootName); Folders.Swap(f.Folders); Files.Swap(f.Files); UidMap.Swap(f.UidMap); Mail.Swap(f.Mail); Remote.Swap(f.Remote); Local.Swap(f.Local); LeafName.Swap(f.LeafName); Sub.Swap(f.Sub); Field.Swap(f.Field); } int ImapFolder::GetLastUid() { uint32_t Max = 0; for (auto it : UidMap) Max = MAX(Max, it.key); return Max; } void ImapFolder::OnSelect(bool b) { if (Mail.State == Store3Loaded) { ImapMsg *m = new ImapMsg(IMAP_SELECT_FOLDER, _FL); if (m) { ImapFolderInfo &i = m->Fld.New(); i.Remote = Remote.Get(); i.Local = Local.Get(); i.LastUid = GetLastUid(); Store->PostThread(m, false); } } } void ImapFolder::OnDeleteComplete() { LgiTrace("OnDeleteComplete %s\n", Local.Get()); // Clear out child folders... while (Sub.Length()) { ImapFolder *f = CastFld(Sub.First()); if (f) { f->OnDeleteComplete(); DeleteObj(f); } else break; } // Delete all the mail files.. while (Mail.Length()) { ImapMail *m = Mail.a[0]; if (m) { m->OnDelete(); DeleteObj(m); } else break; } // Remove ourselves from the parent folder... SetParent(NULL); if (Local) { FileDev->RemoveFolder(Local, true); } } LAutoStreamI ImapFolder::GetStream(const char *file, int line) { LAssert(0); return LAutoStreamI(0); } bool ImapFolder::SetStream(LAutoStreamI stream) { LAssert(0); return false; } const char *ImapFolder::GetStr(int id) { switch (id) { case FIELD_FOLDER_NAME: { if (IsRoot()) { if (!RootName && Store->SettingStore) { LVariant v; if (Store->SettingStore->GetValue(OPT_AccountName, v)) { RootName = v.Str(); } } if (!RootName) { char *User = Store->User.Str(); char *At = User ? strchr(User, '@') : 0; RootName.Printf("%.*s@%s", (int)(At ? At - User : strlen(User)), Store->User.Str(), Store->Host.Str()); } return RootName; } char *d = strrchr(Local, DIR_CHAR); return d ? d + 1 : (char*)"Error"; } } LAssert(0); return NULL; } Store3Status ImapFolder::SetStr(int id, const char *str) { switch (id) { case FIELD_FOLDER_NAME: { return WhenLoaded([this, str](auto Status) { if (IsOnDisk()) { LAutoPtr RenameMsg(new ImapMsg(IMAP_RENAME_FOLDER, _FL)); if (!RenameMsg) return Store3Error; LString l = Local.Get(); ssize_t last = l.RFind(DIR_STR); if (last) l.Length(last); char s[MAX_PATH_LEN]; LMakePath(s, sizeof(s), l, str); RenameMsg->Parent = Remote.Get(); RenameMsg->NewRemote = MakeImapPath(s); Store->PostThread(RenameMsg.Release(), true); return Store3Delayed; } else { LeafName = str; return Store3Success; } }); break; } } LAssert(0); return Store3Error; } #define GetI(p) \ return Meta.GetAsInt(p); \ break; int64 ImapFolder::GetInt(int id) { switch (id) { case FIELD_SYSTEM_FOLDER: return System; case FIELD_IS_ONLINE: return IsOnline || GetParent() == NULL; case FIELD_STORE_TYPE: return Store3Imap; case FIELD_FOLDER_THREAD: #if IMAP_PROTOBUF return ThreadView; #else if (Meta.GetAsInt(PROP_THREAD) < 0) Meta.SetAttr(PROP_THREAD, 0); GetI(PROP_THREAD); #endif case FIELD_FOLDER_PERM_READ: #if IMAP_PROTOBUF return PermRead; #else GetI(PROP_READ); #endif case FIELD_FOLDER_PERM_WRITE: #if IMAP_PROTOBUF return PermWrite; #else GetI(PROP_WRITE); #endif case FIELD_SORT: #if IMAP_PROTOBUF return SortField; #else if (SortCache == INVALID_CACHE) SortCache = Meta.GetAsInt(PROP_SORT); return SortCache; #endif case FIELD_FOLDER_OPEN: #if IMAP_PROTOBUF return Expanded; #else GetI(PROP_EXPANDED); #endif case FIELD_UNREAD: #if IMAP_PROTOBUF return UnreadCount; #else GetI(PROP_UNREAD); #endif case FIELD_FOLDER_TYPE: return ItemType; case FIELD_LOADED: return Mail.State == Store3Loaded; case FIELD_FOLDER_INDEX: return -1; } LAssert(0); return -1; } #define SetI(p) \ Meta.SetAttr(p, (int)i); \ SetDirty(true); \ return Store3Success; \ Store3Status ImapFolder::SetInt(int id, int64 i) { switch (id) { case FIELD_FOLDER_THREAD: return WhenLoaded([this, i](auto Status) { #if IMAP_PROTOBUF ThreadView = i; return Store3Success; #else SetI(PROP_THREAD); #endif }); case FIELD_FOLDER_PERM_READ: return WhenLoaded([this, i](auto Status) { #if IMAP_PROTOBUF PermRead = (ScribePerm)i; return Store3Success; #else SetI(PROP_READ); #endif }); case FIELD_FOLDER_PERM_WRITE: return WhenLoaded([this, i](auto Status) { #if IMAP_PROTOBUF PermWrite = (ScribePerm)i; return Store3Success; #else SetI(PROP_WRITE); #endif }); case FIELD_SORT: return WhenLoaded([this, i](auto Status) { #if IMAP_PROTOBUF SortField = i; return Store3Success; #else SortCache = (int)i; SetI(PROP_SORT); #endif }); case FIELD_FOLDER_OPEN: return WhenLoaded([this, i](auto Status) { #if IMAP_PROTOBUF Expanded = i != 0; return Store3Success; #else SetI(PROP_EXPANDED); #endif }); case FIELD_UNREAD: return WhenLoaded([this, i](auto Status) { #if IMAP_PROTOBUF UnreadCount = i; return Store3Success; #else SetI(PROP_UNREAD); #endif }); case FIELD_FOLDER_TYPE: return WhenLoaded([this, i](auto Status) { ItemType = (int)i; }); break; case FIELD_FOLDER_INDEX: return Store3Error; case FIELD_LOADED: { if (i) { LoadMail(); } else { // Unload folder... Mail.DeleteObjects(); Mail.State = Store3Unloaded; } break; } default: LAssert(0); return Store3Error; } return Store3Success; } const LDateTime *ImapFolder::GetDate(int id) { LAssert(0); return NULL; } Store3Status ImapFolder::SetDate(int id, const LDateTime *i) { LAssert(0); return Store3Error; } LDataPropI *ImapFolder::GetObj(int id) { LAssert(0); return NULL; } -GDataIt ImapFolder::GetList(int id) +LDataIt ImapFolder::GetList(int id) { LAssert(0); return NULL; } LDataIterator &ImapFolder::SubFolders() { LoadSub(); return Sub; } LDataIterator &ImapFolder::Children() { LoadMail(); return Mail; } LDataIterator &ImapFolder::Fields() { return Field; } Store3Status ImapFolder::FreeChildren() { if (Store) { LArray Lst; for (unsigned i=0; iUserData) Lst.Add(t); } if (!Store->Callback->OnDelete(this, Lst)) return Store3Error; } Sub.DeleteObjects(); UidMap.DeleteObjects(); Mail.DeleteObjects(); Mail.State = Store3Unloaded; return Store3Success; } Store3Status ImapFolder::DeleteAllChildren() { // Delete cmd LAutoPtr Msg(new ImapMsg(IMAP_DELETE, _FL)); Msg->Parent = Remote.Get(); if (!Store->PostThread(Msg.Release(), false)) return Store3Error; // Immediately expunge Msg.Reset(new ImapMsg(IMAP_EXPUNGE_FOLDER, _FL)); Msg->Fld[0].Remote = Remote.Get(); if (!Store->PostThread(Msg.Release(), false)) return Store3Error; return Store3Delayed; } ImapFolder *ImapFolder::Find(const char *local, const char *remote) { if (Local.Equals(local) || Remote.Equals(remote)) return this; LoadSub(); for (auto cf: Sub.a) { LAssert(cf->GetParent() == this); ImapFolder *r = cf->Find(local, remote); if (r) return r; } return NULL; } ImapMail *ImapFolder::FindByFile(char *File) { if (!File) return 0; for (unsigned i=0; iPath) { if (!_stricmp(m->Path, File)) return m; } } return 0; } ImapMail::IMeta ImapFolderData::GetMeta(uint32_t id, bool Create) { if (!id) return NULL; auto t = UidMap.Find(id); #if IMAP_PROTOBUF if (!t && Create && (t = new scribe::Email)) { t->set_uid(id); UidMap.Add(id, t); } #else if (!t && Create && (t = new LXmlTag(TAG_MAIL))) { t->SetAttr(ATTR_UID, (int)id); UidMap.Add(id, t); } #endif return t; } bool ImapFolderData::DeleteMeta(uint32_t id) { if (!id) return false; auto t = UidMap.Find(id); if (!t) return false; #ifdef _DEBUG ImapMail *m = GetMail(id); LAssert(m == 0); if (m) DelMail(m); #endif UidMap.Delete(id); DeleteObj(t); return true; } ImapMail *ImapFolderData::GetMail(uint32_t Uid) { if (!Uid) return 0; for (auto m: Mail.a) { if (m->Uid == Uid) return m; } return 0; } bool ImapFolderData::DelMail(ImapMail *m) { if (!m) { LAssert(!"Null ptr."); return false; } LoadMail(); if (Mail.IndexOf(m) < 0) { LAssert(!"Doesn't exist."); return false; } auto t = UidMap.Find(m->Uid); if (t) { #if 0 // def _DEBUG LStackTrace("%s DelMail %s\n", Remote.Get(), m->Uid.Get()); #endif UidMap.Delete(m->Uid); DeleteObj(t); } else { static bool First = true; if (First) { First = false; LAssert(!"There really should be a meta tag."); } } Mail.Delete(m); SetDirty(); return true; } #define DEBUG_PROF_ADD_MAIL 0 #if DEBUG_PROF_ADD_MAIL #define PROFILE(str) Prof.Add(str) #else #define PROFILE(str) #endif bool ImapFolderData::AddMail(ImapMail *m, LArray *OnNew) { #if DEBUG_PROF_ADD_MAIL LProfile Prof("ImapFolder::AddMail", 1); #endif if (!m) { LAssert(!"Null ptr."); return false; } PROFILE("Index chk"); if (Mail.IndexOf(m, true) >= 0) { LAssert(!"Already added."); return false; } PROFILE("Mail insert"); if (!m->Parent) { m->Parent = dynamic_cast(this); LAssert(m->Parent); } Mail.Insert(m, -1, true); if (m->Uid) { #if IMAP_PROTOBUF // UidMap.Add(m->Uid, m); #else PROFILE("GetMeta"); LXmlTag *t = m->GetMeta(false); if (!t && (t = m->GetMeta())) { // Creating a new meta tag... set it's variables. PROFILE("Set attr"); t->SetAttr(ATTR_UID, (int)m->Uid); // UidMap will already have the entry by now... PROFILE("Serialize"); m->Serialize(t, true); } else { PROFILE("Serialize"); m->Serialize(t, false); } #endif } // else its a temp object for appending to a mail box PROFILE("DIR chk"); char *Leaf = m->Path ? strrchr(m->Path, DIR_CHAR) : 0; if (!Leaf) { LAssert(!"Not a valid path."); } if (OnNew) { PROFILE("OnNewAdd"); OnNew->Add(m); } return true; } void ImapFolder::OnPulse() { // int64 Now = LCurrentTime(); if (Dirty) { Save(); } if (Sub.GetState() == Store3Loaded) { for (LDataFolderI *c=Sub.First(); c; c=Sub.Next()) { ImapFolder *f = dynamic_cast(c); f->OnPulse(); } } } void ImapFolder::OnCommand(const char *Name) { if (!Remote) return; ScribeDomType Method = StrToDom(Name); switch (Method) { case SdExpunge: { ImapMsg *m = new ImapMsg(IMAP_EXPUNGE_FOLDER, _FL); m->Fld[0].Remote = Remote.Get(); Store->PostThread(m, false); break; } case SdUndelete: { ImapMsg *m = new ImapMsg(IMAP_UNDELETE, _FL); m->Parent = Remote.Get(); for (auto it : UidMap) { #if IMAP_PROTOBUF LAssert(0); #else char *Flags = it.value->GetAttr(ATTR_FLAGS); if (Flags && stristr(Flags, "/deleted")) { ImapMailInfo &Inf = m->Mail.New(); Inf.Uid = it.key; } #endif } Store->PostThread(m, false); break; } case SdRefresh: { ImapMsg *m = new ImapMsg(IMAP_FOLDER_LISTING, _FL); ImapFolderInfo &f = m->Fld[0]; f.Remote = Remote.Get(); f.LastUid = 0; Store->PostThread(m, false); break; } default: { LAssert(!"Unknown command"); break; } } } void ImapFolder::OnExpunge() { LArray Del; for (auto it : UidMap) { #if IMAP_PROTOBUF LAssert(0); #else char *Flags = it.value->GetAttr(ATTR_FLAGS); if (Flags && stristr(Flags, "/deleted")) Del.Add(it.value); #endif } for (unsigned i=0; iGetAsInt(ATTR_UID); LAssert(Uid > 0); if (Uid) { UidMap.Delete(Uid); DeleteObj(Del[i]); } #endif } } bool ImapFolder::OnRename(const char *NewRemote) { if (!NewRemote || !GetParent()) { LAssert(!"Param error."); return false; } char s[MAX_PATH_LEN]; LMakePath(s, sizeof(s), Store->Cache, NewRemote); if (!FileDev->Move(Local, s)) { LAssert(!"Failed to move folder."); return false; } Local = s; Remote = NewRemote; return true; } /////////////////////////////////////////////////////////////////////////////////// ImapFolderFld::ImapFolderFld(LDataStoreI *s, ImapFolder *p, int id, int width) : LXmlTag("Field") { Parent = p; SetAttr("Id", id); SetAttr("Width", width); } const char *ImapFolderFld::GetStr(int id) { if (id == FIELD_NAME) { switch (GetAsInt("Id")) { case FIELD_CACHE_FILENAME: return "Filename"; case FIELD_CACHE_FLAGS: return "ImapFlags"; } } return 0; } int64 ImapFolderFld::GetInt(int id) { switch (id) { case FIELD_ID: return GetAsInt("Id"); case FIELD_WIDTH: return GetAsInt("Width"); } return -1; } Store3Status ImapFolderFld::SetInt(int id, int64 i) { switch (id) { case FIELD_ID: SetAttr("Id", (int)i); break; case FIELD_WIDTH: SetAttr("Width", (int)i); break; default: return Store3Error; } if (Parent) Parent->SetDirty(); return Store3Success; } diff --git a/Code/Store3Imap/ScribeImap_Mail.cpp b/Code/Store3Imap/ScribeImap_Mail.cpp --- a/Code/Store3Imap/ScribeImap_Mail.cpp +++ b/Code/Store3Imap/ScribeImap_Mail.cpp @@ -1,1489 +1,1489 @@ #include "lgi/common/Lgi.h" #include "lgi/common/NetTools.h" #include "ScribeImap.h" #include "ScribeUtils.h" #include "resdefs.h" #include "lgi/common/LgiRes.h" #include "lgi/common/TextConvert.h" #if IMAP_PROTOBUF #include "include/Scribe.pb.cc" #endif ImapMail::ImapMail(ImapStore *store, const char *file, int line, uint32_t uid) : AllocFile(file), AllocLine(line), From(store), Reply(store) { LocalFlags = 0; Seg = 0; State = ImapMailIdle; Loaded = Store3Unloaded; Parent = 0; SegDirty = false; RemoteFlags.ImapSeen = true; Priority = MAIL_PRIORITY_NORMAL; Store = store; Uid = uid; } ImapMail::~ImapMail() { if (Parent) { Parent->DelMail(this); } DeleteObj(Seg); To.DeleteObjects(); } void ImapMail::SetRemoteFlags(ImapMailFlags f) { RemoteFlags = f; // Replicate to local flags as well... if (f.ImapSeen) LocalFlags |= MAIL_READ; else LocalFlags &= ~MAIL_READ; if (f.ImapAnswered) LocalFlags |= MAIL_REPLIED; else LocalFlags &= ~MAIL_REPLIED; } ImapMail::IMeta ImapMail::GetMeta(bool AllowCreate) { if (Data) return Data->GetMeta(Uid, AllowCreate); if (Parent) return Parent->GetMeta(Uid, AllowCreate); return NULL; } void ImapMail::Serialize(IMeta m, bool Write) { LAssert(m != NULL); if (!m) return; #if IMAP_PROTOBUF if (Write) { } else { } #else if (Write) { // Mail -> Meta m->SetAttr(ATTR_FLAGS, RemoteFlags.Get()); if (DateSent.IsValid()) { char c[32]; DateSent.SetFormat(GDTF_YEAR_MONTH_DAY); DateSent.ToUtc(); DateSent.Get(c, sizeof(c)); DateSent.SetFormat(DateSent.GetDefaultFormat()); ValidateImapDate(DateSent); m->SetAttr(ATTR_DATE, c); } m->SetAttr(ATTR_LABEL, Label); m->SetAttr(ATTR_COLOUR, (int64)Colour.c32()); char *l = Path ? strrchr(Path, DIR_CHAR) : 0; if (l) m->SetContent(l + 1); if (ValidStr(Subject)) m->SetAttr(ATTR_SUBJECT, Subject); else m->DelAttr(ATTR_SUBJECT); LVariant v; if (From.GetVariant("Text", v)) m->SetAttr(ATTR_FROM, v.Str()); if (Reply.GetVariant("Text", v)) m->SetAttr(ATTR_REPLYTO, v.Str()); Parent->SetDirty(); } else { // Meta -> Mail auto Flags = m->GetAttr(ATTR_FLAGS); if (Flags) RemoteFlags.Set(Flags); char *sLocal = m->GetAttr(ATTR_LOCAL); if (sLocal) LocalFlags = atoi(sLocal); else if (Flags) // Synthesis from remote flags LocalFlags = (RemoteFlags.ImapAnswered ? MAIL_REPLIED : 0) | (RemoteFlags.ImapSeen ? MAIL_READ : 0); char *s = m->GetAttr(ATTR_DATE); DateSent.SetFormat(GDTF_YEAR_MONTH_DAY); DateSent.SetTimeZone(0, false); DateSent.Set(s); DateSent.SetFormat(DateSent.GetDefaultFormat()); // Hmmmm: still don't know where these are coming from if (DateSent.Year() == 2000 && DateSent.Hours() == 0) { DateSent.Empty(); if (Parent && !Parent->IsLoading()) Parent->SetDirty(); } DateReceived = DateSent; Label = m->GetAttr(ATTR_LABEL); char *c = m->GetAttr(ATTR_COLOUR); if (c) Colour.Set((uint32_t)atoi64(c), 32); else Colour.Empty(); Subject = m->GetAttr(ATTR_SUBJECT); LVariant FromText = m->GetAttr(ATTR_FROM); #if 1 From.SetVariant("Text", FromText); #else char *sFrom = FromText.Str(); if (sFrom) { LRange Nm(-1), Em(-1); for (char *c = sFrom; *c; c++) { if (*c == '\"') if (Nm.Start < 0) Nm.Start = c - sFrom + 1; else Nm.Len = c - sFrom - Nm.Start; else if (*c == '<') Em.Start = c - sFrom + 1; else if (*c == '>') Em.Len = c - sFrom - Em.Start; } if (Nm.Len) { if (Nm.Start >= 0) From.Name.Set(sFrom + Nm.Start, Nm.Len); else LAssert(0); } if (Em.Len) { if (Em.Start >= 0) From.Addr.Set(sFrom + Em.Start, Em.Len); else LAssert(0); } } #endif if (Parent && m->GetContent()) { // Trip whitespace of the end of the filename... char *e = m->GetContent() + strlen(m->GetContent()); while (e > m->GetContent() && strchr(" \t\r\n", e[-1])) e--; *e = 0; char p[MAX_PATH_LEN]; LMakePath(p, sizeof(p), Parent->Local, m->GetContent()); Path = p; } } #endif } void ImapMail::SetState(ImapMailState s) { /* if (s == ImapMailGettingBody) LgiTrace("%s:%i - SetStart ImapMailGettingBody for %i\n", _FL, Uid); */ State = s; } void ImapMail::Load() { if (Loaded == Store3Unloaded) { LFile f; if (LFileExists(Path) && f.Open(Path, O_READ)) { Loaded = Store3Headers; LArray Buf; int Chunk = 2 << 10; while (!HeaderCache) { Buf.Length(Buf.Length() + Chunk); f.Read(&Buf[Buf.Length()-Chunk], Chunk); auto e = Strnstr(&Buf[0], "\r\n\r\n", Buf.Length()); if (e) { ssize_t HeaderSize = e - &Buf[0]; HeaderCache.Reset(NewStr(&Buf[0], HeaderSize)); break; } if ((int64)Buf.Length() >= f.GetSize()) break; } LAutoString Type(InetGetHeaderField(HeaderCache, "Content-Type")); if (Type) { if (_strnicmp(Type, "multipart/", 10) == 0 && _strnicmp(Type, sMultipartAlternative, 21) != 0) { SetInt(FIELD_FLAGS, LocalFlags | MAIL_ATTACHMENTS); } } } } } Store3Status ImapMail::SetRfc822(LStreamI *m) { DeleteObj(Seg); if (!Stream.Reset(new LTempStream(ScribeTempPath()))) { LgiTrace("%s:%i - Alloc failed.\n", _FL); return Store3Error; } LCopyStreamer Cp; if (!Cp.Copy(m, Stream)) { LgiTrace("%s:%i - LCopyStreamer copy failed.\n", _FL); return Store3Error; } Loaded = Store3Loaded; return Store3Success; } #define DEBUG_READ_MIME 0 void ImapMail::ReadMime(IMeta MetaTag) { #if DEBUG_READ_MIME LProfile Prof("ReadMime"); #endif if (Loaded == Store3Loaded || !Uid) return; auto t = MetaTag ? MetaTag : GetMeta(); if (!t) { LAssert(!"No Meta"); return; } #if DEBUG_READ_MIME Prof.Add(_FL); #endif if (!LFileExists(Path)) { if (State != ImapMailGettingBody) { /*bool InParent =*/ Parent && Parent->GetMail(Uid); // We don't have a local copy of the message, tell the thread to fetch it... ImapMsg *Msg = new ImapMsg(IMAP_DOWNLOAD, _FL); if (Msg) { #if DEBUG_READ_MIME Prof.Add(_FL); #endif ImapMailInfo &Info = Msg->Mail.New(); #if IMAP_PROTOBUF Info.Size = t->size(); #else Info.Size = t->GetAsInt(ATTR_SIZE); #endif Info.Uid = Uid; Info.Local = Path.Get(); Msg->Parent = Parent->Remote.Get(); if (Store->PostThread(Msg, true)) { SetState(ImapMailGettingBody); Loaded = Store3Loading; } else LgiTrace("%s:%i - PostThread failed.\n", _FL); } } } else { // Load the rest of the mime parts Loaded = Store3Loaded; #if DEBUG_READ_MIME Prof.Add(_FL); #endif LStreamI *Load = NULL; LFile f; if (Stream) { Load = Stream; } else if (f.Open(Path, O_READ)) { Load = &f; } #if DEBUG_READ_MIME Prof.Add(_FL); #endif if (Load) { if (Load->GetSize() < 8) { // Hmmm, really? Try and re-download the mail ImapMsg *Msg = new ImapMsg(IMAP_DOWNLOAD, _FL); if (Msg) { Loaded = Store3Headers; #if DEBUG_READ_MIME Prof.Add(_FL); #endif ImapMailInfo &Info = Msg->Mail.New(); #if IMAP_PROTOBUF Info.Size = t->size(); Info.Uid = t->uid(); #else Info.Size = t->GetAsInt(ATTR_SIZE); Info.Uid = t->GetAsInt(ATTR_UID); #endif Info.Local = Path.Get(); Msg->Parent = Parent->Remote.Get(); Store->PostThread(Msg, true); } } else { LAutoPtr Mime(new LMime); if (Mime) { uint64 Start = LCurrentTime(); #if DEBUG_READ_MIME Prof.Add(_FL); #endif Mime->Text.Decode.Pull(Load); DeleteObj(Seg); #if DEBUG_READ_MIME Prof.Add(_FL); #endif Seg = new ImapAttachment(Store, this, Mime.Release()); #if DEBUG_READ_MIME Prof.Add(_FL); #endif if (Seg->IsMixed()) SetInt(FIELD_FLAGS, LocalFlags | MAIL_ATTACHMENTS); uint64 Dur = LCurrentTime() - Start; if (Dur > 100) LgiTrace("%s:%i - MimeDecode: " LPrintfInt64 "\n", _FL, Dur); } } } } } bool ImapMail::FindSegs(const char *Type, LArray &Segs) { if (!Seg) ReadMime(NULL); if (Seg) { Seg->FindSegs(Type, Segs); for (unsigned i=0; iGetSeg()->GetFileName()); if (Fn) { Segs.DeleteAt(i--, true); } } } return Segs.Length() > 0; } static auto DefaultCharset = "windows-1252"; const char *ImapMail::GetStr(int id) { if (State != ImapMailIdle && id != FIELD_INTERNET_HEADER && id != FIELD_MESSAGE_ID && id != FIELD_SERVER_UID) { if (id != FIELD_SUBJECT) return NULL; // LgiTrace("%s:%i - State=%i, Uid=%i(0x%x)\n", _FL, State, Uid, Uid); if (State == ImapMailGettingBody) { static const char *Loading = NULL; if (!Loading) Loading = LLoadString(IDS_LOADING); return Loading; } else if (State == ImapMailDeleting) { static const char *Deleting = NULL; if (!Deleting) Deleting = LLoadString(IDS_DELETING); return Deleting; } else if (State == ImapMailMoving) { return (char*)"Moving..."; } return NULL; } switch (id) { case FIELD_DEBUG: { static char s[64]; sprintf_s(s, sizeof(s), "Imap.ServerUid=%i", Uid); return s; } case FIELD_INTERNET_HEADER: { if (Seg && Seg->GetSeg()) return Seg->GetSeg()->GetHeaders(); if (Path) { if (LFileExists(Path)) { Load(); } else if (State == ImapMailIdle) { // Request the headers from the worker thread... ImapMsg *Msg = new ImapMsg(IMAP_DOWNLOAD, _FL); if (Msg) { auto t = GetMeta(); ImapMailInfo &Info = Msg->Mail.New(); #if IMAP_PROTOBUF Info.Size = t ? t->size() : -1; Info.Uid = t ? t->uid() : 0; #else Info.Size = t ? t->GetAsInt(ATTR_SIZE) : -1; Info.Uid = Uid; #endif Info.Local = Path.Get(); Msg->Parent = Parent->Remote.Get(); if (Store->PostThread(Msg, false)) { SetState(ImapMailGettingBody); Loaded = Store3Loading; } else LgiTrace("%s:%i - PostThread failed.\n", _FL); } } } return HeaderCache; } case FIELD_MESSAGE_ID: { if (!MsgId) { auto m = GetMeta(); if (m) #if IMAP_PROTOBUF MsgId = m->messageid().c_str(); #else MsgId = m->GetAttr(ATTR_MSGID); #endif if (!MsgId) { auto Headers = GetStr(FIELD_INTERNET_HEADER); LAutoString h(DecodeRfc2047(InetGetHeaderField(Headers, "Message-ID"))); MsgId = h; if (MsgId) { LAssert(!strchr(MsgId, '\n')); if (m) { #if IMAP_PROTOBUF m->set_messageid(MsgId); #else m->SetAttr(ATTR_MSGID, MsgId); #endif Parent->SetDirty(); } } } } return MsgId; } case FIELD_SUBJECT: { if (!Subject) { auto Headers = GetStr(FIELD_INTERNET_HEADER); if (!Headers) return LLoadString(IDS_LOADING); LAutoString s(DecodeRfc2047(InetGetHeaderField(Headers, "Subject"))); Subject = s; auto Meta = GetMeta(); if (Meta) Serialize(Meta, true); } return Subject; } case FIELD_CHARSET: { // Conversion is done locally. return "utf-8"; } case FIELD_TEXT: { LArray Results; if (!TextCache && FindSegs("text/plain", Results)) { LStringPipe p; for (auto Segment: Results) { if (Segment->GetInt(FIELD_SIZE) > (512 << 10)) continue; LAutoStreamI Stream = Segment->GetStream(_FL); if (Stream) { LAutoString Buf; auto Size = Stream->GetSize(); if (Size > 0) { Buf.Reset(new char[Size+1]); auto rd = Stream->Read(Buf, Size); if (rd <= 0) continue; Buf.Get()[rd] = 0; LString Charset = Segment->GetStr(FIELD_CHARSET); if (!Charset) Charset = DetectCharset(Buf.Get()); if (!Charset) Charset = DefaultCharset; LAutoString Utf8; if (!Stricmp(Charset.Get(), "utf-8") && LIsUtf8(Buf)) Utf8 = Buf; else Utf8.Reset((char*)LNewConvertCp("utf-8", Buf, Charset, rd)); if (Results.Length() > 1) { p.Push(Utf8); } else { TextCache = Utf8; return TextCache; } } } } TextCache.Reset(p.NewStr()); } return TextCache; break; } case FIELD_HTML_CHARSET: { LArray Results; if (FindSegs("text/html", Results)) return Results[0]->GetStr(FIELD_CHARSET); break; } case FIELD_ALTERNATE_HTML: { LArray Results; if (!HtmlCache && FindSegs("text/html", Results)) { LAutoStreamI s = Results[0]->GetStream(_FL); if (s) { auto Size = s->GetSize(); if (Size > 0) { HtmlCache.Reset(new char[Size+1]); s->Read(HtmlCache, Size); HtmlCache[Size] = 0; } } } return HtmlCache; break; } case FIELD_CACHE_FLAGS: { static char s[96]; int ch = 0; if (RemoteFlags.ImapAnswered) ch += sprintf_s(s+ch, sizeof(s)-ch, "Answ "); if (RemoteFlags.ImapDeleted) ch += sprintf_s(s+ch, sizeof(s)-ch, "Del "); if (RemoteFlags.ImapDraft) ch += sprintf_s(s+ch, sizeof(s)-ch, "Drft "); if (RemoteFlags.ImapFlagged) ch += sprintf_s(s+ch, sizeof(s)-ch, "Flg "); if (RemoteFlags.ImapRecent) ch += sprintf_s(s+ch, sizeof(s)-ch, "Rec "); if (RemoteFlags.ImapSeen) ch += sprintf_s(s+ch, sizeof(s)-ch, "Seen "); if (RemoteFlags.ImapExpunged) ch += sprintf_s(s+ch, sizeof(s)-ch, "Exp "); return s; break; } case FIELD_CACHE_FILENAME: { char *l = Path ? strrchr(Path, DIR_CHAR) : 0; return l ? l + 1 : Path.Get(); } case FIELD_SERVER_UID: { // We allow the app to get the integer UID here as a string // because POP3 UID's ARE strings, so we need to be compatible if (!UidCache) UidCache.Printf("%u", Uid); return UidCache; break; } case FIELD_LABEL: { auto m = GetMeta(); #if IMAP_PROTOBUF return m ? m->label().c_str() : NULL; #else return m ? m->GetAttr(ATTR_LABEL) : NULL; #endif break; } } return NULL; } void CopyImapSegs(ImapMail *Mail, LDataPropI *Dst, LDataPropI *Src) { if (!Dst || !Src) { LAssert(!"Invalid ptrs"); return; } LDataI *DDst = dynamic_cast(Dst); LDataI *DSrc = dynamic_cast(Src); if (!DDst || !DSrc) { LAssert(!"Cast failed."); return; } DDst->CopyProps(*DSrc); - GDataIt DstLst = Dst->GetList(FIELD_MIME_SEG); - GDataIt SrcLst = Src->GetList(FIELD_MIME_SEG); + LDataIt DstLst = Dst->GetList(FIELD_MIME_SEG); + LDataIt SrcLst = Src->GetList(FIELD_MIME_SEG); ImapAttachment *ParentAtt = dynamic_cast(Dst); LMime *ParentMime = ParentAtt->GetSeg(); for (LDataPropI *c = SrcLst->First(); c; c = SrcLst->Next()) { LDataPropI *n = DstLst->Create(Mail->GetStore()); if (n) { CopyImapSegs(Mail, n, c); ImapAttachment *ChildAtt = dynamic_cast(n); ChildAtt->AttachTo(ParentAtt); LMime *ChildMime = ChildAtt->GetSeg(); if (ParentMime && ChildMime) { if (!ParentMime->Insert(ChildMime)) { LAssert(!"Insert failed."); } } } else LAssert(!"Create object failed."); } } Store3CopyImpl(ImapMail) { Priority = (int)p.GetInt(FIELD_PRIORITY); SetInt(FIELD_FLAGS, p.GetInt(FIELD_FLAGS)); SetStr(FIELD_LABEL, p.GetStr(FIELD_LABEL)); int64 Col = p.GetInt(FIELD_COLOUR); SetInt(FIELD_COLOUR, Col); MsgId = p.GetStr(FIELD_MESSAGE_ID); Subject = p.GetStr(FIELD_SUBJECT); LDataPropI *i = p.GetObj(FIELD_FROM); if (i) From.CopyProps(*i); i = p.GetObj(FIELD_REPLY); if (i) Reply.CopyProps(*i); const LDateTime *d = p.GetDate(FIELD_DATE_RECEIVED); if (d) DateReceived = *d; else DateReceived.Empty(); d = p.GetDate(FIELD_DATE_SENT); if (d) { DateSent = *d; ValidateImapDate(DateSent); } else DateSent.Empty(); To.DeleteObjects(); - GDataIt pTo = p.GetList(FIELD_TO); + LDataIt pTo = p.GetList(FIELD_TO); for (unsigned n=0; nLength(); n++) { Store3Addr *a = new Store3Addr(GetStore(), (*pTo)[n]); if (a) To.Insert(a, -1, true); } To.State = Store3Loaded; Seg = dynamic_cast(GetObj(FIELD_MIME_SEG)); if (!Seg) Seg = new ImapAttachment(Store, this); auto SrcSeg = p.GetObj(FIELD_MIME_SEG); if (SrcSeg) CopyImapSegs(this, Seg, SrcSeg); return true; } #define _Str(v) { char *s = NewStr(str); DeleteArray(v); v = s; return !((s != 0) ^ (str != 0)); } Store3Status ImapMail::SetStr(int id, const char *str) { Load(); switch (id) { case FIELD_SUBJECT: Subject = str; break; case FIELD_MESSAGE_ID: { // Set the local copy MsgId = str; // Set the folder meta data copy auto m = GetMeta(); if (m) { #if IMAP_PROTOBUF m->set_messageid(str); #else m->SetAttr(ATTR_MSGID, str); #endif Parent->SetDirty(); } break; } case FIELD_LABEL: { auto m = GetMeta(); if (m) { #if IMAP_PROTOBUF m->set_label(str); #else m->SetAttr(ATTR_LABEL, Label = str); #endif Parent->SetDirty(); } else { Label = str; } break; } case FIELD_SERVER_UID: // ?? break; case FIELD_INTERNET_HEADER: { if (HeaderCache.Get() != str) HeaderCache.Reset(NewStr(str)); // Reset all the cached values... MsgId.Empty(); Subject.Empty(); From.Empty(); Reply.Empty(); DateReceived.Empty(); DateSent.Empty(); To.DeleteObjects(); DeleteObj(Seg); Loaded = Store3Unloaded; break; } case FIELD_ALTERNATE_HTML: { const char *Mt; ImapAttachment *Alt = Seg ? Seg->FindChildByMimeType(sMultipartAlternative) : NULL; ImapAttachment *Html = Alt ? Alt->FindChildByMimeType(sTextHtml) : NULL; if (Html) { if (str) { // Set new stream LAutoStreamI a(new LMemStream(str, strlen(str))); if (Html->SetStream(a)) SegDirty = true; } else { // Delete content Html->Delete(); delete Html; break; } } else if (Seg && (Mt = Seg->GetStr(FIELD_MIME_TYPE)) && !_stricmp(Mt, sTextHtml)) // Is the root segment HTML? { // Set the data to an empty stream... LAutoStreamI a(str ? new LMemStream(str, strlen(str)) : NULL); if (Seg->SetStream(a)) SegDirty = true; } HtmlCache.Reset(); break; } } return Store3Success; } int64 ImapMail::GetInt(int id) { switch (id) { case FIELD_STORE_TYPE: return Store3Imap; case FIELD_LOADED: return Loaded; case FIELD_DONT_SHOW_PREVIEW: return true; case FIELD_SIZE: { auto Meta = GetMeta(); if (Meta) { #if IMAP_PROTOBUF return DataSize = Meta->size(); #else char *Sz = Meta->GetAttr(ATTR_SIZE); if (Sz) return atoi64(Sz); #endif } DataSize = LFileSize(Path); LAssert(DataSize <= 0x7fffffff); return DataSize; } case FIELD_PRIORITY: return Priority; case FIELD_FLAGS: // LgiTrace("%p.LocalFlags = %s\n", this, EmailFlagsToStr(LocalFlags).Get()); return LocalFlags; case FIELD_COLOUR: { uint32_t col = Colour.c32(); auto m = GetMeta(); if (m) { #if IMAP_PROTOBUF col = m->colour(); #else char *s = m->GetAttr(ATTR_COLOUR); if (s) col = (uint32_t)atoi64(s); #endif } return col; break; } case FIELD_ACCOUNT_ID: return Store->GetInt(id); case FIELD_SERVER_UID: return Uid; } return -1; } Store3Status ImapMail::SetInt(int id, int64 i) { Load(); switch (id) { case FIELD_LOADED: { if (Loaded >= i) return Store3Success; if (i == Store3Loaded) ReadMime(NULL); else LAssert(!"What other option is there?"); if (Loaded >= i) return Store3Success; if (Loaded == Store3Loading) return Store3Delayed; return Store3Error; // what happened here? break; } case FIELD_COLOUR: { Colour.Set((uint32_t)i, 32); auto m = GetMeta(); if (m) { #if IMAP_PROTOBUF m->set_colour(i); #else m->SetAttr(ATTR_COLOUR, i); #endif Parent->SetDirty(); } break; } case FIELD_PRIORITY: Priority = (int)i; break; case FIELD_FLAGS: { bool Seen = RemoteFlags.ImapSeen; bool Answered = RemoteFlags.ImapAnswered; LocalFlags = (int)i; RemoteFlags.ImapSeen = TestFlag(i, MAIL_READ); RemoteFlags.ImapAnswered = TestFlag(i, MAIL_REPLIED); if (!Parent) return Store3Success; auto t = GetMeta(); if (t) { #if IMAP_PROTOBUF t->set_remoteflags(RemoteFlags.All); t->set_localflags(RemoteFlags.All); #else t->SetAttr(ATTR_FLAGS, RemoteFlags.Get()); t->SetAttr(ATTR_LOCAL, i); #endif Parent->SetDirty(); } else { LAssert(!"No tag."); return Store3Error; } if ( (RemoteFlags.ImapSeen ^ Seen) || (RemoteFlags.ImapAnswered ^ Answered) ) { // Update the IMAP server LAutoPtr Msg(new ImapMsg(IMAP_SET_FLAGS, _FL)); if (!Msg) return Store3Error; Msg->Parent = Parent->Remote.Get(); LAssert(Msg->Parent); ImapMailInfo &Info = Msg->Mail.New(); Info.Uid = Uid; Info.Flags = RemoteFlags; if (!Store->PostThread(Msg.Release(), false)) return Store3Error; return Store3Delayed; // caller doesn't have to set the object dirty } return Store3Success; } } return Store3Success; } const LDateTime *ImapMail::GetDate(int id) { if ((id == FIELD_DATE_RECEIVED && !DateReceived.Year()) || (id == FIELD_DATE_SENT && !DateSent.Year())) { /* auto m = GetMeta(false); if (m) { auto s = m->GetAttr(ATTR_DATE); int asd=0; } */ auto Headers = GetStr(FIELD_INTERNET_HEADER); LAutoString h(InetGetHeaderField(Headers, "Date")); if (h) { DateReceived.Decode(h); DateReceived.ToUtc(); DateSent = DateReceived; ValidateImapDate(DateSent); } } switch (id) { case FIELD_DATE_RECEIVED: return &DateReceived; case FIELD_DATE_SENT: return &DateSent; } return 0; } Store3Status ImapMail::SetDate(int id, const LDateTime *i) { Load(); switch (id) { case FIELD_DATE_RECEIVED: DateReceived = *i; break; case FIELD_DATE_SENT: DateSent = *i; ValidateImapDate(DateSent); break; default: return Store3Error; } return Store3Success; } Store3Addr *ImapMail::ProcessAddress(Store3Addr &Addr, const char *FieldId, const char *RfcField) { if (!Addr.Addr) { // Try and load from the meta... auto m = GetMeta(); if (m) { #if IMAP_PROTOBUF auto f = m->from(); if (!f.email().empty()) { Addr.SetStr(FIELD_NAME, f.name().c_str()); Addr.SetStr(FIELD_EMAIL, f.email().c_str()); return &Addr; } #else auto f = m->GetAttr(FieldId); if (f) { LAutoString Name, Email; DecodeAddrName(f, Name, Email, 0); Addr.SetStr(FIELD_NAME, Name); Addr.SetStr(FIELD_EMAIL, Email); return &Addr; } #endif } // Otherwise try and load from the headers... auto Headers = GetStr(FIELD_INTERNET_HEADER); LAutoString f(DecodeRfc2047(InetGetHeaderField(Headers, RfcField))); if (f) { LAutoString Name, Email; DecodeAddrName(f, Name, Email, 0); Addr.SetStr(FIELD_NAME, Name); Addr.SetStr(FIELD_EMAIL, Email); if (Uid) Serialize(m, true); } } return &Addr; } LDataPropI *ImapMail::GetObj(int id) { switch (id) { case FIELD_MIME_SEG: { if (!Seg) ReadMime(NULL); return Seg; } case FIELD_FROM: { return ProcessAddress(From, ATTR_FROM, "From"); } case FIELD_REPLY: { return ProcessAddress(Reply, ATTR_REPLYTO, "Reply-To"); } } return 0; } Store3Status ImapMail::SetObj(int id, LDataPropI *i) { LAssert(0); return Store3Error; } -GDataIt ImapMail::GetList(int id) +LDataIt ImapMail::GetList(int id) { Load(); switch (id) { case FIELD_TO: { if (!To.Length()) { auto Headers = GetStr(FIELD_INTERNET_HEADER); LAutoString h(DecodeRfc2047(InetGetHeaderField(Headers, "To"))); if (h) { List Addr; TokeniseStrList(h, Addr, ","); for (auto a: Addr) { Store3Addr *la = new Store3Addr(GetStore()); if (la) { DecodeAddrName(a, la->Name, la->Addr, 0); To.Insert(la, -1, true); } } Addr.DeleteArrays(); } To.State = Store3Loaded; if (h.Reset(DecodeRfc2047(InetGetHeaderField(Headers, "Cc")))) { List Addr; TokeniseStrList(h, Addr, ","); for (auto a: Addr) { Store3Addr *la = new Store3Addr(GetStore()); if (la) { DecodeAddrName(a, la->Name, la->Addr, 0); la->CC = true; To.Insert(la); } } Addr.DeleteArrays(); } } return &To; } } return 0; } Store3Status ImapMail::Save(LDataI *Folder) { Store3Status Status = Store3Error; ImapFolder *Fld = dynamic_cast(Folder); if (!Fld) { LAssert(0); LgiTrace("%s:%i - No folder.\n", _FL); } else if (Path) { LAssert(0); LgiTrace("%s:%i - Already has path?\n", _FL); } else { if (!Seg && !Stream) { // Need to create an empty text segment LMime *m = new LMime(ScribeTempPath()); if (!m) return Store3Error; m->SetMimeType(sTextPlain); if (Subject) m->Set("Subject", Subject); if (DateSent.IsValid()) m->Set("Date", DateSent.Get()); if (MsgId) m->Set("MessageID", MsgId); if (From.Addr) { LVariant v; if (From.GetValue("Text", v)) m->Set("From", v.Str()); } if (Reply.Addr) { LVariant v; if (Reply.GetValue("Text", v)) m->Set("Reply", v.Str()); } Seg = new ImapAttachment(Store, this, m); } char r[32], p[MAX_PATH_LEN]; do { sprintf_s(r, sizeof(r), "temp_%u.eml", LRand()); LMakePath(p, sizeof(p), Fld->Local, r); } while (LFileExists(p)); Path = p; LFile f; if (f.Open(Path, O_WRITE)) { bool SaveFailed = false; if (Stream) { // Copy the import stream directly to disk... LCopyStreamer Cp; Stream->SetPos(0); if (Cp.Copy(Stream, &f)) { // After writing to disk, this is not needed anymore. Stream.Release(); } else { LgiTrace("%s:%i - Copy failed.\n", _FL); SaveFailed = true; } } else if (Seg) { // Re-encode the MIME segment tree... auto s = Seg->GetSeg(); if (!s) { LgiTrace("%s:%i - No segment.\n", _FL); SaveFailed = true; } else if (!s->Text.Encode.Push(&f)) { LgiTrace("%s:%i - Mime encode failed.\n", _FL); SaveFailed = true; } } f.Close(); if (!SaveFailed) { LAutoPtr m(new ImapMsg(IMAP_APPEND, _FL)); m->Parent = Fld->Remote.Get(); m->Mail[0].Local = Path.Get(); m->Mail[0].Flags = RemoteFlags; if (Fld->Store->PostThread(m.Release(), false)) { Status = Store3Delayed; Fld->LoadMail(); Fld->AddMail(this, 0); } else { LgiTrace("%s:%i - PostThread failed.\n", _FL); } } } else { LgiTrace("%s:%i - Failed to open '%s' for writing.\n", _FL, Path.Get()); } } return Status; } Store3Status ImapMail::Delete(bool ToTrash) { Store3Status Status = Store3Error; LAutoPtr m(new ImapMsg(IMAP_DELETE, _FL)); m->Mail[0].Uid = Uid; m->Parent = Parent->Remote.Get(); if (Store->PostThread(m.Release(), false)) { SetState(ImapMailDeleting); Status = Store3Delayed; LArray a; a.Add(this); Store->OnChange(_FL, a, 0); } return Status; } bool ImapMail::OnDelete() { bool Status = false; if (Parent) { Parent->DelMail(this); Parent = 0; } if (Path) { Status = FileDev->Delete(Path, false); } return Status; } LAutoStreamI ImapMail::GetStream(const char *file, int line) { LAutoStreamI Ret; if (Stream) { Ret.Reset(new LProxyStream(Stream)); } else { LFile *f; if (Ret.Reset(f = new LFile)) { if (!f->Open(Path, O_READWRITE)) Ret.Reset(); } } return Ret; } bool ImapMail::SetStream(LAutoStreamI stream) { if (!stream) return false; DeleteObj(Seg); // MIME parse the stream and store it to segments. LAutoPtr Mime(new LMime); if (Mime->Text.Decode.Pull(stream)) { Seg = new ImapAttachment(Store, this, Mime.Release()); if (Seg) { // This stops the objects being written to disk. // Which would mean we have multiple copies of the same // data on disk. This setting also propagates down the // tree automatically as GMimeToStore3 saves new child // notes to their parents. Seg->SetInMemoryOnly(true); Seg->AttachTo(this); } } return false; } void ImapMail::OnDownload(LAutoString &Headers) { LFile f; if (f.Open(Path, O_WRITE)) { f.Write(Headers, strlen(Headers)); f.Close(); } HeaderCache = Headers; Loaded = Store3Headers; SetState(ImapMailIdle); auto t = GetMeta(); if (t) { // t->SetAttr(ATTR_HEADER_ONLY, 1); // This pulls all the relevant fields out of the headers so they can be written // to the meta element. This means it gets loaded without having to touch the // email on disk. Which means faster filtering on the commonly displayed list // fields. GetStr(FIELD_SUBJECT); GetObj(FIELD_FROM); GetDate(FIELD_DATE_SENT); Serialize(t, true); } } diff --git a/Code/Store3Mail2/Mail2_Calendar.cpp b/Code/Store3Mail2/Mail2_Calendar.cpp --- a/Code/Store3Mail2/Mail2_Calendar.cpp +++ b/Code/Store3Mail2/Mail2_Calendar.cpp @@ -1,453 +1,453 @@ #include "Store3Mail2.h" #include "Calendar.h" /* int CalType; // FIELD_CAL_TYPE int Completed; // FIELD_CAL_COMPLETED int ReminderTime; // FIELD_CAL_REMINDER_TIME int ReminderAction; // FIELD_CAL_REMINDER_ACTION int ShowTimeAs; // FIELD_CAL_SHOW_TIME_AS int Recur; // FIELD_CAL_RECUR int RecurFreq; // FIELD_CAL_RECUR_FREQ int RecurInterval; // FIELD_CAL_RECUR_INTERVAL int RecurCount; // FIELD_CAL_RECUR_END_COUNT int RecurEndType; // FIELD_CAL_RECUR_END_TYPE int FilterDays; // FIELD_CAL_RECUR_FILTER_DAYS int FilterMonths; // FIELD_CAL_RECUR_FILTER_MONTHS char *Subject; // FIELD_CAL_SUBJECT char *Location; // FIELD_CAL_LOCATION char *ReminderArg; // FIELD_CAL_REMINDER_ARG char *RecurPos; // FIELD_CAL_RECUR_FILTER_POS char *FilterYears; // FIELD_CAL_RECUR_FILTER_YEARS char *Notes; // FIELD_CAL_NOTES char *Uid; // FIELD_UID LDateTime Start; // FIELD_CAL_START_UTC LDateTime End; // FIELD_CAL_END_UTC LDateTime RecurEnd; // FIELD_CAL_RECUR_END_DATE */ #define AllCalendarFields() \ _Macro(Subject, FIELD_CAL_SUBJECT) \ _Macro(CalType, FIELD_CAL_TYPE) \ _Macro(Completed, FIELD_CAL_COMPLETED) \ _Macro(ReminderTime, FIELD_CAL_REMINDER_TIME) \ _Macro(ReminderAction, FIELD_CAL_REMINDER_ACTION) \ _Macro(ShowTimeAs, FIELD_CAL_SHOW_TIME_AS) \ _Macro(Recur, FIELD_CAL_RECUR) \ _Macro(RecurFreq, FIELD_CAL_RECUR_FREQ) \ _Macro(RecurInterval, FIELD_CAL_RECUR_INTERVAL) \ _Macro(RecurCount, FIELD_CAL_RECUR_END_COUNT) \ _Macro(RecurEndType, FIELD_CAL_RECUR_END_TYPE) \ _Macro(FilterDays, FIELD_CAL_RECUR_FILTER_DAYS) \ _Macro(FilterMonths, FIELD_CAL_RECUR_FILTER_MONTHS) \ _Macro(Location, FIELD_CAL_LOCATION) \ _Macro(ReminderArg, FIELD_CAL_REMINDER_ARG) \ _Macro(RecurPos, FIELD_CAL_RECUR_FILTER_POS) \ _Macro(FilterYears, FIELD_CAL_RECUR_FILTER_YEARS) \ _Macro(Notes, FIELD_CAL_NOTES) \ _Macro(Uid, FIELD_UID) \ _Macro(Start, FIELD_CAL_START_UTC) \ _Macro(End, FIELD_CAL_END_UTC) \ _Macro(RecurEnd, FIELD_CAL_RECUR_END_DATE) ////////////////////////////////////////////////////////////////////// CalendarData::CalendarData(LMail2Store *s) : ThingData(s) { CalType = CAL_EVENT; Completed = false; Subject = 0; Location = 0; ReminderTime = 0; ReminderAction = 0; ReminderArg = 0; ShowTimeAs = 0; Recur = 0; RecurFreq = 0; RecurInterval = 0; RecurCount = 0; RecurEndType = 0; RecurPos = 0; FilterDays = 0; FilterMonths = 0; FilterYears = 0; Notes = 0; TimeZone = 0; Uid = 0; } CalendarData::~CalendarData() { DeleteArray(TimeZone); DeleteArray(Subject); DeleteArray(Location); DeleteArray(Uid); DeleteArray(ReminderArg); DeleteArray(RecurPos); DeleteArray(FilterYears); DeleteArray(Notes); } LDataI &CalendarData::operator =(LDataI &p) { SetStr(FIELD_CAL_SUBJECT, p.GetStr(FIELD_CAL_SUBJECT)); SetStr(FIELD_CAL_LOCATION, p.GetStr(FIELD_CAL_LOCATION)); SetStr(FIELD_CAL_REMINDER_ARG, p.GetStr(FIELD_CAL_REMINDER_ARG)); SetStr(FIELD_CAL_RECUR_FILTER_POS, p.GetStr(FIELD_CAL_RECUR_FILTER_POS)); SetStr(FIELD_CAL_RECUR_FILTER_YEARS, p.GetStr(FIELD_CAL_RECUR_FILTER_YEARS)); SetStr(FIELD_CAL_NOTES, p.GetStr(FIELD_CAL_NOTES)); SetStr(FIELD_CAL_TIMEZONE, p.GetStr(FIELD_CAL_TIMEZONE)); SetStr(FIELD_UID, p.GetStr(FIELD_UID)); SetInt(FIELD_CAL_RECUR_FILTER_MONTHS, p.GetInt(FIELD_CAL_RECUR_FILTER_MONTHS)); SetInt(FIELD_CAL_RECUR_FILTER_DAYS, p.GetInt(FIELD_CAL_RECUR_FILTER_DAYS)); SetInt(FIELD_CAL_TYPE, p.GetInt(FIELD_CAL_TYPE)); SetInt(FIELD_CAL_COMPLETED, p.GetInt(FIELD_CAL_COMPLETED)); SetInt(FIELD_CAL_REMINDER_TIME, p.GetInt(FIELD_CAL_REMINDER_TIME)); SetInt(FIELD_CAL_REMINDER_ACTION, p.GetInt(FIELD_CAL_REMINDER_ACTION)); SetInt(FIELD_CAL_SHOW_TIME_AS, p.GetInt(FIELD_CAL_SHOW_TIME_AS)); SetInt(FIELD_CAL_RECUR, p.GetInt(FIELD_CAL_RECUR)); SetInt(FIELD_CAL_RECUR_FREQ, p.GetInt(FIELD_CAL_RECUR_FREQ)); SetInt(FIELD_CAL_RECUR_INTERVAL, p.GetInt(FIELD_CAL_RECUR_INTERVAL)); SetInt(FIELD_CAL_RECUR_END_COUNT, p.GetInt(FIELD_CAL_RECUR_END_COUNT)); SetInt(FIELD_CAL_RECUR_END_TYPE, p.GetInt(FIELD_CAL_RECUR_END_TYPE)); SetDate(FIELD_CAL_START_UTC, p.GetDate(FIELD_CAL_START_UTC)); SetDate(FIELD_CAL_END_UTC, p.GetDate(FIELD_CAL_END_UTC)); SetDate(FIELD_CAL_RECUR_END_DATE, p.GetDate(FIELD_CAL_RECUR_END_DATE)); return *this; } void CalendarData::Load() { if (IsLoaded) return; if (Store) { IsLoaded = true; LFile *f = Store->GotoObject(_FL); if (f) { Serialize(*f, false); DeleteObj(f); } } } char *CalendarData::GetStr(int id) { Load(); switch (id) { case FIELD_CAL_SUBJECT: return Subject; case FIELD_CAL_LOCATION: return Location; case FIELD_CAL_REMINDER_ARG: return ReminderArg; case FIELD_CAL_RECUR_FILTER_POS: return RecurPos; case FIELD_CAL_RECUR_FILTER_YEARS: return FilterYears; case FIELD_CAL_NOTES: return Notes; case FIELD_CAL_TIMEZONE: return TimeZone; case FIELD_UID: return Uid; } LgiTrace("%s:%i - Unknown id %i\n", _FL, id); LAssert(0); return 0; } bool CalendarData::SetStr(int id, const char *str) { Load(); switch (id) { case FIELD_CAL_TIMEZONE: _Str(TimeZone); case FIELD_CAL_SUBJECT: _Str(Subject); case FIELD_CAL_LOCATION: _Str(Location); case FIELD_CAL_REMINDER_ARG: _Str(ReminderArg); case FIELD_CAL_RECUR_FILTER_POS: _Str(RecurPos); case FIELD_CAL_RECUR_FILTER_YEARS: _Str(FilterYears); case FIELD_CAL_NOTES: _Str(Notes); case FIELD_UID: _Str(Uid); } LgiTrace("%s:%i - Unknown id %i\n", _FL, id); LAssert(0); return 0; } int64 CalendarData::GetInt(int id) { Load(); switch (id) { case FIELD_IS_IMAP: return false; case FIELD_CAL_RECUR_FILTER_MONTHS: return FilterMonths; case FIELD_CAL_RECUR_FILTER_DAYS: return FilterDays; case FIELD_CAL_TYPE: return CalType; case FIELD_CAL_COMPLETED: return Completed; case FIELD_CAL_REMINDER_TIME: return ReminderTime; case FIELD_CAL_REMINDER_ACTION: return ReminderAction; case FIELD_CAL_SHOW_TIME_AS: return ShowTimeAs; case FIELD_CAL_RECUR: return Recur; case FIELD_CAL_RECUR_FREQ: return RecurFreq; case FIELD_CAL_RECUR_INTERVAL: return RecurInterval; case FIELD_CAL_RECUR_END_COUNT: return RecurCount; case FIELD_CAL_RECUR_END_TYPE: return RecurEndType; } LgiTrace("%s:%i - Unknown id %i\n", _FL, id); LAssert(0); return -1; } bool CalendarData::SetInt(int id, int64 i) { Load(); switch (id) { case FIELD_CAL_RECUR_FILTER_MONTHS: FilterMonths = (int)i; return true; case FIELD_CAL_RECUR_FILTER_DAYS: FilterDays = (int)i; return true; case FIELD_CAL_TYPE: CalType = (int)i; return true; case FIELD_CAL_COMPLETED: Completed = (int)i; return true; case FIELD_CAL_REMINDER_TIME: ReminderTime = (int)i; return true; case FIELD_CAL_REMINDER_ACTION: ReminderAction = (int)i; return true; case FIELD_CAL_SHOW_TIME_AS: ShowTimeAs = (int)i; return true; case FIELD_CAL_RECUR: Recur = (int)i; return true; case FIELD_CAL_RECUR_FREQ: RecurFreq = (int)i; return true; case FIELD_CAL_RECUR_INTERVAL: RecurInterval = (int)i; return true; case FIELD_CAL_RECUR_END_COUNT: RecurCount = (int)i; return true; case FIELD_CAL_RECUR_END_TYPE: RecurEndType = (int)i; return true; case FIELD_UID: { char s[64]; sprintf_s(s, sizeof(s), LGI_PrintfInt64, i); return SetStr(FIELD_UID, s); } } LgiTrace("%s:%i - Unknown id %i\n", _FL, id); LAssert(0); return 0; } LDateTime *CalendarData::GetDate(int id) { Load(); switch (id) { case FIELD_CAL_START_UTC: return Start.Year() ? &Start : 0; case FIELD_CAL_END_UTC: return End.Year() ? &End : 0; case FIELD_CAL_RECUR_END_DATE: return RecurEnd.Year() ? &RecurEnd : 0; } LgiTrace("%s:%i - Unknown id %i\n", _FL, id); LAssert(0); return 0; } bool CalendarData::SetDate(int id, LDateTime *t) { if (!t) return false; Load(); switch (id) { case FIELD_CAL_START_UTC: Start = *t; return true; case FIELD_CAL_END_UTC: End = *t; return true; case FIELD_CAL_RECUR_END_DATE: RecurEnd = *t; return true; } LgiTrace("%s:%i - Unknown id %i\n", _FL, id); LAssert(0); return 0; } -GDataIt CalendarData::GetList(int id) +LDataIt CalendarData::GetList(int id) { Load(); LgiTrace("%s:%i - Unknown id %i\n", _FL, id); LAssert(0); return 0; } int CalendarData::Type() { return MAGIC_CALENDAR; } int CalendarData::Sizeof() { int Size = sizeof(uint32) + // magic sizeof(uint32); // number of fields #define _Macro(var, id) { int s = ThingData::Sizeof(var); Size += s; \ /*LgiTrace("sizeof(%i)=%i\n", id, s);*/ } AllCalendarFields(); #undef _Macro return Size; } bool CalendarData::Serialize(LFile &f, bool Write) { bool Status = true; uint32 Magic = Type(); if (Write) { f << Magic; uint32 Fields = 22; f << Fields; #define _Macro(var, id) { int w = ThingData::Write(f, id, var); \ /*LgiTrace("write(%i)=%i\n", id, w);*/ } AllCalendarFields(); #undef _Macro } else { uint32 n; f >> n; if (n == Magic) { f >> n; for (uint32 i=0; i> Id; f >> Type; switch (Type) { case OBJ_STRING: { uint32 Size; f >> Size; char *Buf = new char[Size+1]; if (Buf) { f.Read(Buf, Size); Buf[Size] = 0; SetStr(Id, Buf); DeleteArray(Buf); } break; } case OBJ_BINARY: { uint32 Size; f >> Size; if (Size == sizeof(Mail2Date)) { Mail2Date d; f >> d.Day; f >> d.Month; f >> d.Year; f >> d.Hour; f >> d.Minute; f >> d.ThouSec; LDateTime dt; dt.Day(d.Day); dt.Month(d.Month); dt.Year(d.Year); dt.Hours(d.Hour); dt.Minutes(d.Minute); dt.Seconds(d.ThouSec / 1000); dt.Thousands(d.ThouSec % 1000); SetDate(Id, &dt); } break; } case OBJ_INT: { int k; f >> k; SetInt(Id, k); break; } } } } else LAssert(0); } return Status; } diff --git a/Code/Store3Mail2/Mail2_Contact.cpp b/Code/Store3Mail2/Mail2_Contact.cpp --- a/Code/Store3Mail2/Mail2_Contact.cpp +++ b/Code/Store3Mail2/Mail2_Contact.cpp @@ -1,319 +1,319 @@ #include "Store3Mail2.h" #include "LMap.h" #define ForAllContactFields(var) ItemFieldDef *var = 0; for (var = ContactFieldDefs; var->Option && var->FieldId; var++) static bool Init = false; static const char *IdToOpt[FIELD_TIMEZONE+1]; #define GetOpt(id) ( (id >= 0 && id < CountOf(IdToOpt) ) ? IdToOpt[id] : 0 ) ////////////////////////////////////////////////////////////////////// ContactData::ContactData(LMail2Store *s) : ThingData(s) { if (!Init) { Init = true; ZeroObj(IdToOpt); for (ItemFieldDef *d = ContactFieldDefs; d->FieldId; d++) { if (d->FieldId < CountOf(IdToOpt)) { IdToOpt[d->FieldId] = d->Option; } else LAssert(0); } } } ContactData::~ContactData() { AltEmail.DeleteArrays(); Plugins.DeleteArrays(); } LDataI &ContactData::operator =(LDataI &p) { ForAllContactFields(i) { SetStr(i->FieldId, p.GetStr(i->FieldId)); } return *this; } void ContactData::Load() { if (IsLoaded) return; if (Store) { IsLoaded = true; LFile *f = Store->GotoObject(_FL); if (f) { Serialize(*f, false); DeleteObj(f); } } } char *ContactData::GetStr(int id) { Load(); const char *Opt = GetOpt(id); if (Opt) { char *s; return Get(Opt, s) ? s : 0; } if (id == FIELD_ALT_EMAIL) { LStringPipe p; for (unsigned i=0; iOption); } for (unsigned i=0; iOption, s) && ValidStr(s)) Items++; } } f << (ulong) MAGIC_CONTACT; f << (ulong) (Items + Plugins.Length() + AltEmail.Length()); LMap Done; ForAllContactFields(Fld) { char *c; if (Get(Fld->Option, c) && ValidStr(c)) { #if defined(_DEBUG) && defined(WIN32) && !defined(_WIN64) if (Done[Fld->Id()]) { _asm int 3 // Um, you've already used that ID } Done[Fld->Id()] = 1; #endif f << ((short) Fld->FieldId); WriteStr(f, c); } } for (unsigned n=0; n> Magic; if (Magic == MAGIC_CONTACT) { ulong Fields = 0; f >> Fields; for (unsigned i=0; i> Id; if (Id == FIELD_PLUGIN_ASSOC) { // Plugin Association Field char *s = ReadStr(f PassDebugArgs); if (s) { Plugins.Add(s); } } else if (Id == FIELD_ALT_EMAIL) { // Alt Email Field char *s = ReadStr(f PassDebugArgs); if (s) { AltEmail.Add(s); } } else { // General contact field ForAllContactFields(fld) { if (fld->FieldId == Id) { // process field char *c = ReadStr(f PassDebugArgs); if (c && strlen(c) > 0) { Set(fld->Option, c); DeleteArray(c); } fld = 0; break; } } if (fld) { // didn't find field int Size = 0; f >> Size; if (Size <= 0) { break; } else { f.Seek(Size, SEEK_CUR); } } } } } else { Status = false; } } return Status; } diff --git a/Code/Store3Mail2/Mail2_Filter.cpp b/Code/Store3Mail2/Mail2_Filter.cpp --- a/Code/Store3Mail2/Mail2_Filter.cpp +++ b/Code/Store3Mail2/Mail2_Filter.cpp @@ -1,588 +1,588 @@ #include "Store3Mail2.h" #define COMBINE_OP_AND 0 #define COMBINE_OP_OR 1 enum FilterInFlags { FilterIncoming = 0x1, FilterInternal = 0x2, }; static const char *OpNames[] = { "=", "!=", "<", "<=", ">=", ">", "Like", // LLoadString(IDS_LIKE), "Contains", // LLoadString(IDS_CONTAINS), "Starts With", // LLoadString(IDS_STARTS_WITH), "Ends With", // LLoadString(IDS_ENDS_WITH), 0 }; ////////////////////////////////////////////////////////////////////// FilterData::FilterData(LMail2Store *s) : ThingData(s) { Flags = 0; Index = 0; Name = 0; Script = 0; StopFiltering = true; ConditionsXml = 0; FilterFlags = FilterIncoming | FilterInternal; Outgoing = false; } FilterData::~FilterData() { DeleteArray(Name); DeleteArray(ConditionsXml); DeleteArray(Script); Actions.DeleteObjects(); } LDataI &FilterData::operator =(LDataI &p) { SetStr(FIELD_FILTER_NAME, p.GetStr(FIELD_FILTER_NAME)); SetStr(FIELD_FILTER_CONDITIONS_XML, p.GetStr(FIELD_FILTER_CONDITIONS_XML)); SetStr(FIELD_FILTER_SCRIPT, p.GetStr(FIELD_FILTER_SCRIPT)); SetInt(FIELD_FLAGS, p.GetInt(FIELD_FLAGS)); SetInt(FIELD_STOP_FILTERING, p.GetInt(FIELD_STOP_FILTERING)); SetInt(FIELD_FILTER_INDEX, p.GetInt(FIELD_FILTER_INDEX)); SetInt(FIELD_FILTER_INCOMING, p.GetInt(FIELD_FILTER_INCOMING)); SetInt(FIELD_FILTER_OUTGOING, p.GetInt(FIELD_FILTER_OUTGOING)); SetInt(FIELD_FILTER_INTERNAL, p.GetInt(FIELD_FILTER_INTERNAL)); return *this; } void FilterData::Load() { if (IsLoaded) return; if (Store) { IsLoaded = true; LAutoPtr f(Store->GotoObject(_FL)); if (f) { Serialize(*f, false); } } } char *FilterData::GetStr(int id) { Load(); switch (id) { case FIELD_FILTER_NAME: return Name; case FIELD_FILTER_CONDITIONS_XML: return ConditionsXml; case FIELD_FILTER_SCRIPT: return Script; case FIELD_FILTER_ACTIONS_XML: { // Convert actions into xml LStringPipe p; LXmlTag r("Actions"); for (unsigned i=0; i t(new LXmlTag("Action")); if (Actions.a[i]->Get(t)) { r.InsertTag(t.Release()); } } LXmlTree t; t.Write(&r, &p); ActionXml.Reset(p.NewStr()); return ActionXml; break; } } LAssert(0); return 0; } bool FilterData::SetStr(int id, const char *str) { Load(); switch (id) { case FIELD_FILTER_NAME: _Str(Name); case FIELD_FILTER_CONDITIONS_XML: _Str(ConditionsXml); case FIELD_FILTER_SCRIPT: _Str(Script); case FIELD_FILTER_ACTIONS_XML: { // Convert XML into actions Actions.DeleteObjects(); LMemStream m((char*)str, strlen(str), false); LXmlTree t; LXmlTag r; if (t.Read(&r, &m)) { for (LXmlTag *c = r.Children.First(); c; c = r.Children.Next()) { LAutoPtr a(new FilterAction(Kit)); if (a->Set(c)) { Actions.Insert(a.Release()); } } return true; } else { LgiTrace("%s:%i - Action parsing failed.\n", _FL); return false; } break; } } LAssert(0); return 0; } int64 FilterData::GetInt(int id) { Load(); switch (id) { case FIELD_IS_IMAP: return false; case FIELD_FLAGS: return Flags; case FIELD_STOP_FILTERING: return StopFiltering; case FIELD_FILTER_INDEX: return Index; case FIELD_FILTER_INCOMING: return (FilterFlags & FilterIncoming) != 0; case FIELD_FILTER_INTERNAL: return (FilterFlags & FilterInternal) != 0; case FIELD_FILTER_OUTGOING: return Outgoing; } LAssert(0); return -1; } bool FilterData::SetInt(int id, int64 i) { Load(); switch (id) { case FIELD_FLAGS: Flags = (int)i; return true; case FIELD_STOP_FILTERING: StopFiltering = (uint8)i; return true; case FIELD_FILTER_INDEX: Index = (int)i; return true; case FIELD_FILTER_INCOMING: if (i) FilterFlags |= FilterIncoming; else FilterFlags &= ~FilterIncoming; return true; case FIELD_FILTER_INTERNAL: if (i) FilterFlags |= FilterInternal; else FilterFlags &= ~FilterInternal; return true; case FIELD_FILTER_OUTGOING: Outgoing = (uint8)i; return true; } LAssert(0); return 0; } -GDataIt FilterData::GetList(int id) +LDataIt FilterData::GetList(int id) { Load(); switch (id) { case FIELD_ACTION: return &Actions; } LAssert(0); return 0; } int FilterData::Type() { return MAGIC_FILTER; } int Action_Sizeof(FilterAction *a) { int Size = sizeof(ulong) + // magic: MAGIC_ACTION sizeof(ulong) + // number of fields SizeIntField(a->Type) + SizeStrField(a->Arg1); return Size; } bool Action_Serialize(FilterAction *a, LFile &f, bool Write) { ulong Magic = MAGIC_ACTION; if (Write) { f << Magic; f << ((ulong) 2); WriteIntField(FIELD_ACT_TYPE, a->Type); WriteStrField(FIELD_ACT_ARG, a->Arg1); } else { f >> Magic; if (Magic == MAGIC_ACTION) { ulong Fields = 0; f >> Fields; for (unsigned i=0; i> FieldId; switch (FieldId) { ReadIntField(FIELD_ACT_TYPE, (int&)a->Type); ReadAutoStrField(FIELD_ACT_ARG, a->Arg1); default: { // skip over unknown chunk int Size; f >> Size; f.Seek(Size, SEEK_CUR); } } } } else return false; } return f.GetStatus(); } int FilterData::Sizeof() { uint8 v; int Size = sizeof(ulong) + // magic sizeof(ulong) + // number of fields SizeIntField(Flags) + SizeIntField(Index) + SizeStrField(Name) + SizeIntField(v) + // Incoming SizeIntField(v) + // Outgoing SizeIntField(StopFiltering) + SizeStrField(ConditionsXml) + (Script ? SizeStrField(Script) : 0); for (unsigned i=0; i> Magic; if (Magic == MAGIC_CONDITION) { ulong Fields = 0; f >> Fields; int OldField = -1; for (unsigned i=0; i> FieldId; switch (FieldId) { ReadIntField(FIELD_COND_FIELD, OldField); ReadIntField(FIELD_COND_OPERATOR, Op); ReadStrField(FIELD_COND_VALUE, Value); ReadIntField(FIELD_COND_NOT, Not); ReadStrField(FIELD_COND_SOURCE, Source); default: { // skip over unknown chunk int Size; f >> Size; f.Seek(Size, SEEK_CUR); } } } if (OldField >= 0) { // Convert field into source #define Cf(To) Source = NewStr("mail." To); ForCondField(Cf, OldField); } } else return false; } return f.GetStatus(); } }; bool FilterData::Serialize(LFile &f, bool Write) { bool Status = false; ulong Magic = Type(); if (Write) { f << Magic; f << ((ulong) 8 + Actions.Length() + (Script ? 1 : 0) ); WriteIntField(FIELD_FLAGS, Flags); WriteIntField(FIELD_STOP_FILTERING, StopFiltering); // WriteIntField(FIELD_COMBINE_OP, CombineOp); WriteIntField(FIELD_FILTER_INDEX, Index); WriteStrField(FIELD_FILTER_NAME, Name); WriteStrField(FIELD_FILTER_CONDITIONS_XML, ConditionsXml); WriteIntField(FIELD_FILTER_INCOMING, FilterFlags); WriteIntField(FIELD_FILTER_OUTGOING, Outgoing); if (Script) { WriteStrField(FIELD_FILTER_SCRIPT, Script); } for (unsigned i=0; i> Magic; if (Magic == MAGIC_FILTER) { ulong Fields = 0; f >> Fields; uint8 In = true; uint8 Out = false; LArray OldConditions; int CombineOp = 0; for (unsigned i=0; iEndOfObj(f); i++) { short FieldId = 0; ulong FieldSize = 0; f >> FieldId; switch (FieldId) { ReadIntField(FIELD_FLAGS, Flags); ReadIntField(FIELD_COMBINE_OP, CombineOp); ReadStrField(FIELD_FILTER_NAME, Name); ReadIntField(FIELD_FILTER_INDEX, Index); ReadStrField(FIELD_FILTER_SCRIPT, Script); ReadIntField(FIELD_STOP_FILTERING, StopFiltering); ReadIntField(FIELD_FILTER_INCOMING, In); ReadIntField(FIELD_FILTER_OUTGOING, Out); ReadStrField(FIELD_FILTER_CONDITIONS_XML, ConditionsXml); case FIELD_CONDITION: { f >> FieldSize; FilterDataCondition *c = new FilterDataCondition; if (c) { c->Serialize(f, Write); OldConditions.Add(c); } break; } case FIELD_ACTION: { f >> FieldSize; LAutoPtr a(new FilterAction(GetStore())); if (a && Action_Serialize(a, f, Write)) { Actions.Insert(a.Release()); } break; } default: { // skip over unknown chunk int Size; f >> Size; f.Seek(Size, SEEK_CUR); } } } // Do we need to upgrade old filter conditions to new XML ones? if (OldConditions.Length() > 0) { // Convert to XML LXmlTag *r = new LXmlTag("Conditions"); if (r) { LXmlTag *o = new LXmlTag((char*)(CombineOp == COMBINE_OP_AND ? "And" : "Or")); if (o) { r->InsertTag(o); for (unsigned i=0; iSetAttr("Not", (int)c->Not); n->SetAttr("Field", c->Source); n->SetAttr("Op", OpNames[c->Op]); n->SetAttr("Value", c->Value); o->InsertTag(n); } } LStringPipe p; LXmlTree t; if (t.Write(r, &p)) { DeleteArray(ConditionsXml); ConditionsXml = p.NewStr(); } } } OldConditions.DeleteObjects(); } Status = true; } } return Status; } diff --git a/Code/Store3Mail2/Mail2_Mail.cpp b/Code/Store3Mail2/Mail2_Mail.cpp --- a/Code/Store3Mail2/Mail2_Mail.cpp +++ b/Code/Store3Mail2/Mail2_Mail.cpp @@ -1,1056 +1,1056 @@ #include "Store3Mail2.h" #include "../MailBuf.cpp" #include "INetTools.h" const char sAlternative[] = "multipart/alternative"; MailData::MailData(LMail2Store *s) : ThingData(s), From(s), Reply(s) { Flags = 0; MarkColour = 0; AccountId = 0; Priority = MAIL_PRIORITY_NORMAL; Label = 0; FwdMsgId = 0; ServerUid = 0; Subject = 0; Body = 0; BodyCharset = 0; Html = 0; HtmlCharset = 0; MessageID = 0; InternetHeader = 0; References = 0; Seg = 0; BounceMessageID = NULL; } MailData::~MailData() { Empty(); DeleteObj(Seg); } void MailData::Empty() { DeleteArray(Subject); DeleteArray(Body); DeleteArray(BodyCharset); DeleteArray(Html); DeleteArray(HtmlCharset); DeleteArray(Label); DeleteArray(InternetHeader); DeleteArray(MessageID); DeleteArray(BounceMessageID); DeleteArray(References); DeleteArray(ServerUid); From.Empty(); To.DeleteObjects(); } bool MailData::ParseHeaders() { DeleteArray(Subject); DeleteArray(Label); DeleteArray(MessageID); DeleteArray(BounceMessageID); DeleteArray(References); DeleteArray(ServerUid); From.Empty(); To.DeleteObjects(); Subject = DecodeRfc2047(InetGetHeaderField(InternetHeader, "Subject")); MessageID = DecodeRfc2047(InetGetHeaderField(InternetHeader, "Message-Id")); LAssert(!MessageID || !strchr(MessageID, '\n')); LAutoString s(InetGetHeaderField(InternetHeader, "Date")); if (s) DateSent.Decode(s); else DateSent.Empty(); if (s.Reset(InetGetHeaderField(InternetHeader, "X-Priority"))) Priority = atoi(s); if (s.Reset(InetGetHeaderField(InternetHeader, "X-Color"))) { char *Hash = strchr(s, '#'); if (Hash) { int Rgb = htoi(Hash+1); MarkColour = Rgb32((Rgb >> 16) & 0xff, (Rgb >> 8) & 0xff, Rgb & 0xff); } } if (s.Reset(InetGetHeaderField(InternetHeader, "Disposition-Notification-To"))) Flags |= MAIL_READ_RECEIPT; if (s.Reset(DecodeRfc2047(InetGetHeaderField(InternetHeader, "From")))) DecodeAddrName(s, From.Name, From.Addr, 0); if (s.Reset(DecodeRfc2047(InetGetHeaderField(InternetHeader, "To")))) { List Addr; TokeniseStrList(s, Addr, ","); for (char *RawAddr = Addr.First(); RawAddr; RawAddr = Addr.Next()) { LAutoPtr a(To.Create(Kit)); if (a) { Store3Addr *sa = dynamic_cast(a.Get()); LAssert(sa != NULL); if (sa) { DecodeAddrName(RawAddr, sa->Name, sa->Addr, 0); To.Insert(a.Release()); } } } Addr.DeleteArrays(); } return true; } static char *StreamToString(LStreamI *s) { int64 Len = s->GetSize(); char *Str = new char[(size_t)Len+1]; if (Str) { int Read = s->Read(Str, (int)Len); if (Read <= 0) { DeleteArray(Str); } else Str[Read] = 0; } return Str; } void CopyAttachments(MailData *Mail, LDataI *Source, AttachmentData *&Mixed, int Depth, bool InMultipart = false) { char *Mt = Source->GetStr(FIELD_MIME_TYPE); if (InMultipart && Mt && _strnicmp(Mt, "text/", 5)) { // Copy attachment if (!Mixed && (Mixed = new AttachmentData(Mail->Kit))) { Mixed->SetStr(FIELD_MIME_TYPE, "multipart/mixed"); Mixed->AttachTo(Mail); } if (Mixed) { AttachmentData *a = new AttachmentData(Mail->Kit); if (a) { *a = *Source; a->AttachTo(Mixed); } else LAssert(!"Alloc error"); } else LAssert(!"No mixed segment to attach to."); } else if (Depth == 0) { char *Charset = Source->GetStr(FIELD_CHARSET); GAutoStreamI Data = Source->GetStream(_FL); if (Data) { if (Mt && !_stricmp(Mt, "text/html")) { // Html body Mail->Html = StreamToString(Data); Mail->HtmlCharset = NewStr(Charset); } else { // Text body Mail->Body = StreamToString(Data); Mail->BodyCharset = NewStr(Charset); } } // else this can happen with an empty mime segment. } InMultipart = Mt && !_strnicmp(Mt, "multipart/", 10) && _stricmp(Mt, sAlternative); // Iterate over tree of segments - GDataIt Children = Source->GetList(FIELD_MIME_SEG); + LDataIt Children = Source->GetList(FIELD_MIME_SEG); for (LDataPropI *i=Children->First(); i; i=Children->Next()) { LDataI *d = dynamic_cast(i); if (d) CopyAttachments(Mail, d, Mixed, Depth + 1, InMultipart); } } void PrintMimeTree(AttachmentData *a, const char *msg, int depth = 1) { if (a) { #ifdef _DEBUG if (msg) LgiTrace("MimeTree: %s\n", msg); char m[256]; memset(m, ' ', depth * 4); m[depth * 4] = 0; LgiTrace("%s%s\n", m, a->GetStr(FIELD_MIME_TYPE)); LArray c = a->GetChildren(); for (unsigned i=0; iLength(); n++) { To.Insert(new Mail2Addr(GetStore(), (*pTo)[n])); } DeleteObj(Seg); LDataI *Root = dynamic_cast(p.GetObj(FIELD_MIME_SEG)); if (Root) { AttachmentData *Mixed = 0; CopyAttachments(this, Root, Mixed, 0); } // PrintMimeTree(Seg, "Before"); RebuildMimeTree(); // PrintMimeTree(Seg, "After"); return *this; } bool MailData::RebuildMimeTree() { LAutoString ContentType(InetGetHeaderField(InternetHeader, "Content-Type")); if (ContentType) { char *Colon = strchr(ContentType, ';'); if (Colon) { while (strchr(" \t", Colon[-1])) Colon--; *Colon = 0; } if (!Seg && (Seg = new AttachmentData(Kit))) { Seg->SetStr(FIELD_MIME_TYPE, ContentType); } } int NewFlags = Flags; if (Store) { if (!Seg) { if ((Seg = new AttachmentData(Kit))) { Seg->SetStr(FIELD_MIME_TYPE, "multipart/mixed"); } } if (Seg) { for (StorageItem *c = Store->GetChild(); c; c = c->GetNext()) { if (c->GetType() == MAGIC_ATTACHMENT) { AttachmentData *a = new AttachmentData(Kit); if (a) { a->IsLoaded = false; a->Store = c; c->Object = a; a->AttachTo(Seg); NewFlags |= MAIL_ATTACHMENTS; } } } } } if (Seg) { AttachmentData *TextSeg = 0; AttachmentData *HtmlSeg = 0; if (Seg->IsPlainText()) { TextSeg = Seg; } else if (Seg->IsHtml()) { HtmlSeg = Seg; } else { if (Body && Html) { AttachmentData *Alt; AttachmentData *ChildAlt = Seg->FindChildByMimeType(sAlternative); if (Seg->IsAlternative()) { Alt = Seg; } else if (ChildAlt) { Alt = ChildAlt; } else if ((Alt = new AttachmentData(Kit))) { Alt->SetPlaceholder(); Alt->AttachTo(Seg); } if (Alt) { Alt->SetStr(FIELD_MIME_TYPE, sAlternative); if ((TextSeg = new AttachmentData(Kit))) { TextSeg->AttachTo(Alt); } if ((HtmlSeg = new AttachmentData(Kit))) { HtmlSeg->AttachTo(Alt); } } } else if (Body) { if ((TextSeg = new AttachmentData(Kit))) { TextSeg->AttachTo(Seg); } } else if (Html) { if ((HtmlSeg = new AttachmentData(Kit))) { HtmlSeg->AttachTo(Seg); } } } if (TextSeg) { TextSeg->SetStr(FIELD_MIME_TYPE, "text/plain"); TextSeg->SetExtern(&Body, &BodyCharset); } if (HtmlSeg) { HtmlSeg->SetStr(FIELD_MIME_TYPE, "text/html"); HtmlSeg->SetExtern(&Html, &HtmlCharset); } } if (NewFlags != Flags) { Flags = NewFlags; SetDirty(); } return true; } bool MailData::Load() { if (IsLoaded) return true; if (!Store) return false; LFile *f = Store->GotoObject(_FL); if (f) { Serialize(*f, false); DeleteObj(f); } RebuildMimeTree(); return IsLoaded = true; } Store3Status MailData::Save(LDataI *Folder) { if (!ThingData::Save(Folder)) return Store3Error; if (Seg) Seg->OnSave(); if (Store->GetChild()) Flags |= MAIL_ATTACHMENTS; else Flags &= ~MAIL_ATTACHMENTS; return Store3Success; } int MailData::Type() { return MAGIC_MAIL; } char *MailData::GetStr(int id) { Load(); switch (id) { case FIELD_SUBJECT: return Subject; case FIELD_TEXT: return Body; case FIELD_CHARSET: return BodyCharset; case FIELD_ALTERNATE_HTML: return Html; case FIELD_HTML_CHARSET: return HtmlCharset; case FIELD_LABEL: return Label; case FIELD_INTERNET_HEADER: return InternetHeader; case FIELD_REFERENCES: return References; case FIELD_FWD_MSG_ID: return FwdMsgId; case FIELD_SERVER_UID: return ServerUid; case FIELD_MESSAGE_ID: { if (!MessageID && InternetHeader) { char *Header = InetGetHeaderField(InternetHeader, "Message-ID"); if (Header) { List Ids; ParseIdList(Header, Ids); MessageID = Ids.First(); Ids.Delete(MessageID); Ids.DeleteArrays(); DeleteArray(Header); } } return MessageID; } case FIELD_BOUNCE_MSG_ID: return BounceMessageID; } LAssert(0); return 0; } bool MailData::SetStr(int id, const char *str) { Load(); switch (id) { case FIELD_SUBJECT: _Str(Subject); case FIELD_TEXT: _Str(Body); case FIELD_CHARSET: _Str(BodyCharset); case FIELD_ALTERNATE_HTML: _Str(Html); case FIELD_HTML_CHARSET: _Str(HtmlCharset); case FIELD_LABEL: _Str(Label); case FIELD_MESSAGE_ID: _Str(MessageID); case FIELD_REFERENCES: _Str(References); case FIELD_FWD_MSG_ID: _Str(FwdMsgId); case FIELD_SERVER_UID: _Str(ServerUid); case FIELD_INTERNET_HEADER: DeleteArray(InternetHeader); InternetHeader = NewStr(str); ParseHeaders(); return (InternetHeader != 0) == (str != 0); case FIELD_BOUNCE_MSG_ID: _Str(BounceMessageID); break; } LAssert(0); return false; } int64 MailData::GetInt(int id) { Load(); switch (id) { case FIELD_IS_IMAP: return false; case FIELD_LOADED: return Store3Loaded; case FIELD_SIZE: { int s = Sizeof(); if (Store) { for (StorageItem *i = Store->GetChild(); i; i = i->GetNext()) { s += i->GetObjectSize(); } } return s; } case FIELD_PRIORITY: return Priority; case FIELD_FLAGS: return Flags; case FIELD_DONT_SHOW_PREVIEW: return false; case FIELD_ACCOUNT_ID: return AccountId; case FIELD_MARK_COLOUR: return MarkColour; } LAssert(0); return -1; } bool MailData::SetInt(int id, int64 i) { Load(); switch (id) { case FIELD_DEBUG: Debug = i != 0; return true; case FIELD_PRIORITY: Priority = i; return true; case FIELD_FLAGS: Flags = i; return true; case FIELD_ACCOUNT_ID: AccountId = i; return true; case FIELD_MARK_COLOUR: MarkColour = i; return true; } LAssert(0); return 0; } LDateTime *MailData::GetDate(int id) { Load(); switch (id) { case FIELD_DATE_RECEIVED: return &DateReceived; case FIELD_DATE_SENT: return &DateSent; } LAssert(0); return 0; } bool MailData::SetDate(int id, LDateTime *t) { Load(); switch (id) { case FIELD_DATE_RECEIVED: if (t) DateReceived = *t; else DateReceived.Year(0); return true; case FIELD_DATE_SENT: if (t) DateSent = *t; else DateSent.Year(0); return true; } LAssert(0); return 0; } LDataPropI *MailData::GetObj(int id) { Load(); switch (id) { case FIELD_FROM: return &From; case FIELD_REPLY: return &Reply; case FIELD_MIME_SEG: { if (!Seg) { AttachmentData *a = new AttachmentData(Kit); if (a) { a->SetStr(FIELD_MIME_TYPE, "multipart/mixed"); a->AttachTo(this); } } return Seg; break; } } LAssert(0); return 0; } -GDataIt MailData::GetList(int id) +LDataIt MailData::GetList(int id) { Load(); switch (id) { case FIELD_TO: return &To; } LAssert(0); return 0; } int MailData::Sizeof() { int Size = sizeof(uint32); // magic Size += sizeof(uint32); // number of fields // This causes the msg id to be parsed out of the headers if it hasn't already // FIXME GetMsgId(); // Required fields Size += SizeIntField(Flags); Size += SizeIntField(Priority); Size += SizeIntField(MarkColour); Size += SizeObjField(From); Size += SizeObjField(Reply); Size += SizeObjField(DateReceived); Size += SizeObjField(DateSent); // Optional fields Size += (Subject ? SizeStrField(Subject) : 0); Size += (Body ? SizeStrField(Body) : 0); Size += (MessageID ? SizeStrField(MessageID) : 0); Size += (BounceMessageID ? SizeStrField(BounceMessageID) : 0); Size += (InternetHeader ? SizeStrField(InternetHeader) : 0); Size += (BodyCharset ? SizeStrField(BodyCharset) : 0); Size += (Html ? SizeStrField(Html) : 0); Size += (Label ? SizeStrField(Label) : 0); Size += (References ? SizeStrField(References) : 0); Size += (FwdMsgId ? SizeStrField(FwdMsgId) : 0); Size += (ServerUid ? SizeStrField(ServerUid) : 0); Size += (HtmlCharset ? SizeStrField(HtmlCharset) : 0); Size += (AccountId ? SizeIntField(AccountId) : 0); // List fields for (LDataPropI *a = To.First(); a; a = To.Next()) { Mail2Addr *la = dynamic_cast(a); LAssert(la != NULL); Size += SizeObjField(*la); } return Size; } bool MailData::Serialize(LFile &Stream, bool Write) { uint32 Magic = Type(); if (Write) { LFile &f = Stream; f << Magic; // number of fields following f << (uint32)(7 + ((Subject) ? 1 : 0) + ((Body) ? 1 : 0) + ((MessageID) ? 1 : 0) + ((BounceMessageID) ? 1 : 0) + ((InternetHeader) ? 1 : 0) + ((BodyCharset) ? 1 : 0) + ((Html) ? 1 : 0) + ((Label) ? 1 : 0) + ((References) ? 1 : 0) + ((FwdMsgId) ? 1 : 0) + ((ServerUid) ? 1 : 0) + ((HtmlCharset) ? 1 : 0) + ((AccountId) ? 1 : 0) + To.Length()); WriteIntField(FIELD_FLAGS, Flags); WriteIntField(FIELD_PRIORITY, Priority); WriteIntField(FIELD_MARK_COLOUR, MarkColour); WriteObjField(FIELD_FROM, From); WriteObjField(FIELD_REPLY, Reply); WriteDateField(FIELD_DATE_RECEIVED, DateReceived, false); WriteDateField(FIELD_DATE_SENT, DateSent, false); if (Subject) WriteStrField(FIELD_SUBJECT, Subject); if (Body) WriteStrField(FIELD_TEXT, Body); if (MessageID) WriteStrField(FIELD_MESSAGE_ID, MessageID); if (BounceMessageID) WriteStrField(FIELD_BOUNCE_MSG_ID, BounceMessageID); if (InternetHeader) WriteStrField(FIELD_INTERNET_HEADER, InternetHeader); if (BodyCharset) WriteStrField(FIELD_CHARSET, BodyCharset); if (Html) WriteStrField(FIELD_ALTERNATE_HTML, Html); if (Label) WriteStrField(FIELD_LABEL, Label); if (References) WriteStrField(FIELD_REFERENCES, References); if (FwdMsgId) WriteStrField(FIELD_FWD_MSG_ID, FwdMsgId); if (ServerUid) WriteStrField(FIELD_SERVER_UID, ServerUid); if (HtmlCharset) WriteStrField(FIELD_HTML_CHARSET, HtmlCharset); if (AccountId) WriteIntField(FIELD_ACCOUNT_ID, AccountId); for (LDataPropI *a = To.First(); a; a = To.Next()) { Mail2Addr *la = dynamic_cast(a); switch (la->CC) { case MAIL_ADDR_CC: { WriteObjField(FIELD_CC, *la); break; } case MAIL_ADDR_BCC: { WriteObjField(FIELD_BCC, *la); break; } default: { WriteObjField(FIELD_TO, *la); break; } } } } else { #ifndef MAC int64 OldPos = Stream.GetPos(); MailBuf f(Stream, Store->GetObjectSize()); #else LFile &f = Stream; #endif f >> Magic; if (Magic == MAGIC_MAIL) { short OldCodePage = -1; ulong Fields = 0; f >> Fields; LAssert(InternetHeader == 0); ulong i = 0; uint16 FieldId; int32 FieldSize; for (; i> FieldId; // char *Ptr = Buf.GetPtr(); switch (FieldId) { ReadObjField(FIELD_FROM, From); ReadObjField(FIELD_REPLY, Reply); ReadDateField(FIELD_DATE_RECEIVED, DateReceived, false); ReadDateField(FIELD_DATE_SENT, DateSent, false); ReadStrField(FIELD_SUBJECT, Subject); ReadStrField(FIELD_MESSAGE_ID, MessageID); ReadStrField(FIELD_BOUNCE_MSG_ID, BounceMessageID); ReadStrField(FIELD_INTERNET_HEADER, InternetHeader); ReadIntField(FIELD_FLAGS, Flags); ReadIntField(FIELD_PRIORITY, Priority); ReadIntField(FIELD_MARK_COLOUR, MarkColour); ReadStrField(FIELD_LABEL, Label); ReadIntField(FIELD_CODE_PAGE, OldCodePage); ReadStrField(FIELD_REFERENCES, References); ReadStrField(FIELD_FWD_MSG_ID, FwdMsgId); ReadStrField(FIELD_SERVER_UID, ServerUid); ReadIntField(FIELD_ACCOUNT_ID, AccountId); ReadStrField(FIELD_TEXT, Body); ReadStrField(FIELD_CHARSET, BodyCharset); ReadStrField(FIELD_ALTERNATE_HTML, Html); ReadStrField(FIELD_HTML_CHARSET, HtmlCharset); case FIELD_TO: case FIELD_CC: case FIELD_BCC: { int32 FieldSize; f >> FieldSize; Mail2Addr *a = new Mail2Addr(GetStore()); if (a) { if (a->Serialize(f, Write)) { // a->OnFind(&Contact::Everyone, true); switch (FieldId) { case FIELD_CC: { a->CC = MAIL_ADDR_CC; break; } case FIELD_BCC: { a->CC = MAIL_ADDR_BCC; break; } default: { a->CC = MAIL_ADDR_TO; break; } } To.Insert(a); } else { printf("%s:%i - ListAddr::Serialize failed.\n", __FILE__, __LINE__); DeleteObj(a); } } break; } case FIELD_ADDRESSED_TO: { int32 Size; f >> Size; f.Seek(Size, SEEK_CUR); break; } default: { f >> FieldSize; f.Seek(FieldSize, SEEK_CUR); // FieldSize = *((int32*&)Ptr)++; // Ptr += FieldSize; // printf("%s:%i - Invalid field id '%i' when reading message.\n", __FILE__, __LINE__, FieldId); // i = Fields; break; } } } if (!f.GetStatus() || Fields < 1) { printf("%s:%i - read %lu of %i fields, status=%i\n", __FILE__, __LINE__, i, (int)Fields, f.GetStatus()); } if (OldCodePage >= 0) { // Convert to a new charset string instead of a INT const char *OldCp[] = { "us-ascii", "iso-8859-1", "iso-8859-2", "iso-8859-3", "iso-8859-4", "iso-8859-5", "iso-8859-6", "iso-8859-7", "iso-8859-8", "iso-8859-9", "iso-8859-15", "windows-1250", "windows-1252", "utf-8" }; if (OldCodePage < CountOf(OldCp)) { BodyCharset = NewStr(OldCp[OldCodePage]); } } } else { return false; } } return Stream.GetStatus(); } bool ConnectMime(MailData *Mail, AttachmentData *Parent, LMime *Mime) { if (!Mail || !Mime) return false; AttachmentData *a = new AttachmentData(Mail->Kit); if (!a) return false; LAutoString Mt(Mime->GetMimeType()); LAutoString Charset(Mime->GetCharset()); LAutoString FileName(DecodeRfc2047(Mime->GetFileName())); LAutoString ContentId(Mime->Get("Content-Id")); a->SetStr(FIELD_MIME_TYPE, Mt); a->SetStr(FIELD_NAME, FileName); if (ContentId) { LAutoString Tmp(TrimStr(ContentId, "<>")); a->SetStr(FIELD_CONTENT_ID, Tmp); } if (a->IsMultipart()) { a->SetPlaceholder(); a->SetStr(FIELD_CHARSET, Charset); } else { // Single non multipart body if (a->IsPlainText() && !Mail->Body) { a->SetExtern(&Mail->Body, &Mail->BodyCharset); } else if (a->IsHtml() && !Mail->Html) { a->SetExtern(&Mail->Html, &Mail->HtmlCharset); } a->SetStr(FIELD_CHARSET, Charset); GAutoStreamI dt(Mime->GetData(true)); a->SetStream(dt); } if (Parent) { a->AttachTo(Parent); } else { a->AttachTo(Mail); } for (int i=0; iLength(); i++) { LMime *c = (*Mime)[i]; if (!ConnectMime(Mail, a, c)) return false; } return true; } bool MailData::SetMime(LAutoPtr m) { // Convert a parsed email into the right fields bool Status = false; if (!m) return false; LVariant AutoDeleteExe; Kit->Callback->GetOptions()->GetValue(OPT_AutoDeleteExe, AutoDeleteExe); if ((Status = ConnectMime(this, 0, m))) { // ParseHeaders(); } return Status; } diff --git a/Code/Store3Mail2/Store3Mail2.h b/Code/Store3Mail2/Store3Mail2.h --- a/Code/Store3Mail2/Store3Mail2.h +++ b/Code/Store3Mail2/Store3Mail2.h @@ -1,558 +1,558 @@ #ifndef _STORE3_MAIL2_H_ #define _STORE3_MAIL2_H_ #include "Lgi.h" #include "Scribe.h" #include "Store2.h" #include "Store3Common.h" extern char *HtmlToText(char *Html, char *InitialCharSet); extern char *TextToHtml(char *Txt, char *Charset); #define FIELD_CONDITION 41 #define FIELD_ACTION 42 #define FIELD_COND_FIELD 43 #define FIELD_COND_OPERATOR 44 #define FIELD_COND_VALUE 45 #define FIELD_COND_SOURCE 51 #define FIELD_COND_NOT 53 class FolderData; class ThingData; class AttachmentData; #define _Str(var) \ if (var != str) { DeleteArray(var); var = NewStr(str); } return (var != 0) == (str != 0); #ifdef WIN32 #pragma pack(push, before_pack) #pragma pack(1) #endif struct Mail2Date { uint8 Day; uint8 Month; int16 Year; uint8 Hour; uint8 Minute; uint16 ThouSec; }; #ifdef WIN32 #pragma pack(pop, before_pack) #endif class Mail2Addr : public Store3Addr { public: Mail2Addr(LDataStoreI *store, LDataPropI *i = 0) : Store3Addr(store, i) { } int Sizeof() { return SizeofStr(Name) + SizeofStr(Addr); } bool Serialize(LFile &f, bool Write) { bool Status = true; if (Write) { WriteStr(f, Name); WriteStr(f, Addr); } else { Name.Reset(ReadStr(f PassDebugArgs)); Addr.Reset(ReadStr(f PassDebugArgs)); } return Status; } }; class Mail2Field : public Store3Field { public: Mail2Field(LDataStoreI *Store) : Store3Field(Store) { } int Sizeof() { return sizeof(Id) + sizeof(Width); } bool Serialize(LFile &f, bool Write) { if (Write) { f << Id; f << Width; } else { f >> Id; f >> Width; } return true; } }; class LMail2Store : public LDataStoreI, public Storage2::StorageKitImpl { friend class FolderData; friend class MailData; friend class ThingData; FolderData *Mailbox; LDataEventsI *Callback; LAutoString ErrorMsg; public: LMail2Store(char *file, LDataEventsI *callback); ~LMail2Store(); LDataEventsI *GetEvents() { return Callback; } bool OnIdle() { return false; } uint64 Size(); char *GetStr(int id); bool SetStr(int id, const char *str); int64 GetInt(int id); bool SetInt(int id, int64 i); LDataI *Create(int Type); LDataFolderI *GetRoot(bool Create); FolderData *GetFolder(char *Path); FolderData *CastFolder(StorageItem *i); Store3Status Delete(LArray &Items, bool ToTrash); Store3Status Move(LDataFolderI *NewFolder, LArray &Items); Store3Status Change(LArray &Items, int PropId, LVariant &Value); bool Compact(LViewI *Parent, LDataPropI *Props); void OnEvent(void *Param); }; /// This class glues the mail2 backend to the virtual store3 API class FolderData : public LDataFolderI, public StorageObj { bool Debug; public: // Glue members LMail2Store *Kit; // Folder data memebers int32 Sort; uint8 Open; uint32 ItemType; uint32 UnRead; int16 Index; char *Name; ScribePerm ReadAccess, WriteAccess; uint8 Threaded; Store3SystemFolder System; // Collections DIterator Field; DIterator Sub; DIterator Things; bool Load(bool Flds, bool Things); // Iterators GDataIterator &SubFolders(); GDataIterator &Children(); GDataIterator &Fields(); // Methods FolderData(LMail2Store *store); ~FolderData(); LDataI &operator =(LDataI &p); bool IsOrphan() { return Store == 0; } int Type(); uint64 Size() { return Store ? Store->GetTotalSize() : Sizeof(); } bool IsOnDisk() { return Store != 0; } char *GetStr(int id); bool SetStr(int id, const char *str); int64 GetInt(int id); bool SetInt(int id, int64 i); int Sizeof(); bool Serialize(LFile &f, bool Write); Store3Status DeleteAllChildren(); Store3Status FreeChildren(); FolderData *GetParent() { StorageItem *p = Store ? Store->GetParent() : 0; return p ? dynamic_cast(p->Object) : 0; } // Stubs Store3Status Save(LDataI *Folder = 0); Store3Status Delete(); LDataStoreI *GetStore() { return Kit; } GAutoStreamI GetStream(const char *file, int line) { return GAutoStreamI(0); } }; class ThingData : public LDataI, public StorageObj { protected: bool Debug; public: // Glue members LMail2Store *Kit; bool IsLoaded; ThingData(LMail2Store *s) { Debug = false; Kit = s; IsLoaded = true; } LDataStoreI *GetStore() { return Kit; } bool IsOrphan() { return Store == 0; } uint64 Size() { return Store ? Store->GetTotalSize() : 0; } bool IsOnDisk() { return Store != 0; } Store3Status Save(LDataI *Folder = 0); GAutoStreamI GetStream(const char *file, int line); Store3Status Delete(); int Sizeof(int32 i) { return sizeof(int16) + // Id sizeof(int8) + // Type sizeof(int32); // Data } int Write(LFile &f, int16 Id, int32 i) { uint8 Type = OBJ_INT; int w = f.Write(&Id, sizeof(Id)); w += f.Write(&Type, sizeof(Type)); w += f.Write(&i, sizeof(i)); return w; } int Sizeof(char *s) { return sizeof(int16) + // Id sizeof(int8) + // Type sizeof(int32) + // Size (s ? strlen(s) : 0); // String data } int Write(LFile &f, int16 Id, char *s) { uint8 Type = OBJ_STRING; int32 Size = s ? strlen(s) : 0; int w = f.Write(&Id, sizeof(Id)); w += f.Write(&Type, sizeof(Type)); w += f.Write(&Size, sizeof(Size)); if (s) w += f.Write(s, Size); return w; } int Sizeof(LDateTime &t) { return sizeof(int16) + // Id sizeof(int8) + // Type sizeof(int32) + // Size sizeof(Mail2Date); // Date } int Write(LFile &f, int16 Id, LDateTime &dt) { uint8 Type = OBJ_BINARY; Mail2Date d; int32 Size = sizeof(d); int w = f.Write(&Id, sizeof(Id)); w += f.Write(&Type, sizeof(Type)); w += f.Write(&Size, sizeof(Size)); d.Year = dt.Year(); d.Month = dt.Month(); d.Day = dt.Day(); d.Hour = dt.Hours(); d.Minute = dt.Minutes(); d.ThouSec = dt.Seconds() + (1000 * dt.Thousands()); w += f.Write(&d, Size); return w; } FolderData *GetParent() { StorageItem *Si = Store ? Store->GetParent() : 0; FolderData *Par = Si ? dynamic_cast(Si->Object) : 0; return Par; } int Sizeof() { LAssert(0); return 0; } bool Serialize(LFile &f, bool Write) { LAssert(0); return 0; } LDataI &operator =(LDataI &p) { LAssert(0); return *this; } int Type() { LAssert(0); return 0; } bool SetStream(GAutoStreamI stream) { LAssert(0); return 0; } }; /// This class glues the mail2 backend to the virtual store3 API class MailData : public ThingData { public: // Mail members uint32 Flags; char *Label; char *FwdMsgId; char *ServerUid; char *Subject; char *Body; char *BodyCharset; char *Html; char *HtmlCharset; char *MessageID; char *BounceMessageID; char *InternetHeader; char *References; LDateTime DateReceived; LDateTime DateSent; uint32 AccountId; uint8 Priority; uint32 MarkColour; // FIELD_MARK_COLOUR, 32bit rgba colour Mail2Addr From; Mail2Addr Reply; DIterator To; AttachmentData *Seg; MailData(LMail2Store *s); ~MailData(); LDataI &operator =(LDataI &p); bool Load(); bool ParseHeaders(); /// This function reconstructs a plausible MIME tree /// from the flat format that mail2 saves as. bool RebuildMimeTree(); void Empty(); Store3Status Save(LDataI *Folder = 0); int Type(); char *GetStr(int id); bool SetStr(int id, const char *str); int64 GetInt(int id); bool SetInt(int id, int64 i); LDateTime *GetDate(int id); bool SetDate(int id, LDateTime *t); LDataPropI *GetObj(int id); - GDataIt GetList(int id); + LDataIt GetList(int id); bool SetMime(LAutoPtr m); int Sizeof(); bool Serialize(LFile &Stream, bool Write); }; class AttachmentData : public Store3Attachment, public StorageObj { bool PlaceHolder; uint32 Content; uint32 DataSize; char *Name; char *MimeType; char *ContentId; char *Charset; char **MailBody; char **MailCharset; LStreamI *Import; LAutoString InternetHeaderCache; void Load(); public: bool IsLoaded; AttachmentData(LMail2Store *store); ~AttachmentData(); void SetExtern(char **body = 0, char **charset = 0); void SetPlaceholder() { PlaceHolder = true; Dirty = false; } LArray GetChildren(); // Store3Attachment impl void OnSave(); // StorageObj impl int Type() { return MAGIC_ATTACHMENT; } int Sizeof(); bool Serialize(LFile &Stream, bool Write); // LDataI impl LDataI &operator =(LDataI &p); bool IsOnDisk(); bool IsOrphan(); uint64 Size(); Store3Status Save(LDataI *Parent); Store3Status Delete(); GAutoStreamI GetStream(const char *file, int line); bool SetStream(GAutoStreamI s); // LDataPropI int64 GetInt(int id); bool SetInt(int id, int64 i); char *GetStr(int id); bool SetStr(int id, const char *str); }; class ContactData : public ThingData, public ObjProperties { LAutoPtr AltEmailCache; public: // Glue members LArray Plugins; LArray AltEmail; ContactData(LMail2Store *s); ~ContactData(); LDataI &operator =(LDataI &p); int SizeofField(const char *Name); void Load(); char *GetStr(int id); bool SetStr(int id, const char *str); int64 GetInt(int id); bool SetInt(int id, int64 i); - GDataIt GetList(int id); + LDataIt GetList(int id); int Type(); int Sizeof(); bool Serialize(LFile &f, bool Write); }; class CalendarData : public ThingData { public: int CalType; // FIELD_CAL_TYPE int Completed; // FIELD_CAL_COMPLETED LDateTime Start; // FIELD_CAL_START_UTC LDateTime End; // FIELD_CAL_END_UTC char *TimeZone; // FIELD_CAL_TIMEZONE char *Subject; // FIELD_CAL_SUBJECT char *Location; // FIELD_CAL_LOCATION char *Uid; // FIELD_UID int ReminderTime; // FIELD_CAL_REMINDER_TIME int ReminderAction; // FIELD_CAL_REMINDER_ACTION char *ReminderArg; // FIELD_CAL_REMINDER_ARG int ShowTimeAs; // FIELD_CAL_SHOW_TIME_AS int Recur; // FIELD_CAL_RECUR int RecurFreq; // FIELD_CAL_RECUR_FREQ int RecurInterval; // FIELD_CAL_RECUR_INTERVAL LDateTime RecurEnd; // FIELD_CAL_RECUR_END_DATE int RecurCount; // FIELD_CAL_RECUR_END_COUNT int RecurEndType; // FIELD_CAL_RECUR_END_TYPE char *RecurPos; // FIELD_CAL_RECUR_FILTER_POS int FilterDays; // FIELD_CAL_RECUR_FILTER_DAYS int FilterMonths; // FIELD_CAL_RECUR_FILTER_MONTHS char *FilterYears; // FIELD_CAL_RECUR_FILTER_YEARS char *Notes; // FIELD_CAL_NOTES CalendarData(LMail2Store *s); ~CalendarData(); LDataI &operator =(LDataI &p); void Load(); char *GetStr(int id); bool SetStr(int id, const char *str); int64 GetInt(int id); bool SetInt(int id, int64 i); LDateTime *GetDate(int id); bool SetDate(int id, LDateTime *t); - GDataIt GetList(int id); + LDataIt GetList(int id); int Type(); int Sizeof(); bool Serialize(LFile &f, bool Write); }; class FilterData : public ThingData { public: // Glue members int Flags; int Index; char *Name; char *Script; uint8 StopFiltering; char *ConditionsXml; DIterator Actions; LAutoString ActionXml; uint8 FilterFlags; uint8 Outgoing; // int CombineOp; // List Conditions; FilterData(LMail2Store *s); ~FilterData(); LDataI &operator =(LDataI &p); void Load(); char *GetStr(int id); bool SetStr(int id, const char *str); int64 GetInt(int id); bool SetInt(int id, int64 i); - GDataIt GetList(int id); + LDataIt GetList(int id); int Type(); int Sizeof(); bool Serialize(LFile &f, bool Write); }; class GroupData : public ThingData { public: // Glue members int Flags; char *Name; char *Group; GroupData(LMail2Store *s); ~GroupData(); LDataI &operator =(LDataI &p); void Load(); char *GetStr(int id); bool SetStr(int id, const char *str); int64 GetInt(int id); bool SetInt(int id, int64 i); int Type(); int Sizeof(); bool Serialize(LFile &f, bool Write); }; #endif \ No newline at end of file diff --git a/Code/Store3Mail3/Mail3.h b/Code/Store3Mail3/Mail3.h --- a/Code/Store3Mail3/Mail3.h +++ b/Code/Store3Mail3/Mail3.h @@ -1,775 +1,775 @@ #ifndef _MAIL3_H_ #define _MAIL3_H_ #include "lgi/common/Lgi.h" #include "Store3Common.h" #include "v3.6.14/sqlite3.h" // Debugging stuff #define MAIL3_TRACK_OBJS 0 #define MAIL3_DB_FILE "Database.sqlite" #define MAIL3_TBL_FOLDER "Folder" #define MAIL3_TBL_FOLDER_FLDS "FolderFields" #define MAIL3_TBL_MAIL "Mail" #define MAIL3_TBL_MAILSEGS "MailSegs" #define MAIL3_TBL_CONTACT "Contact" #define MAIL3_TBL_GROUP "ContactGroup" #define MAIL3_TBL_FILTER "Filter" #define MAIL3_TBL_CALENDAR "Calendar" class LMail3Store; class LMail3Mail; enum Mail3SubFormat { Mail3v1, // All the mail and segs in a single tables. Mail3v2, // All the mail and segs in per folders tables. }; struct GMail3Def { const char *Name; const char *Type; }; struct GMail3Idx { const char *IdxName; const char *Table; const char *Column; }; class Mail3BlobStream : public LStream { LMail3Store *Store; sqlite3_blob *b; int64 Pos, Size; int SegId; bool WriteAccess; bool OpenBlob(); bool CloseBlob(); public: const char *File; int Line; Mail3BlobStream(LMail3Store *Store, int segId, int size, const char *file, int line, bool Write = false); ~Mail3BlobStream(); int64 GetPos(); int64 SetPos(int64 p); int64 GetSize(); int64 SetSize(int64 sz); ssize_t Read(void *Buf, ssize_t Len, int Flags = 0); ssize_t Write(const void *Buf, ssize_t Len, int Flags = 0); }; extern GMail3Def TblFolder[]; extern GMail3Def TblFolderFlds[]; extern GMail3Def TblMail[]; extern GMail3Def TblMailSegs[]; extern GMail3Def TblContact[]; extern GMail3Def TblFilter[]; extern GMail3Def TblGroup[]; extern GMail3Def TblCalendar[]; #define SERIALIZE_STR(Var, Col) \ if (Write) \ { \ if (!s.SetStr(Col, Var.Str())) \ return false; \ } \ else \ Var = s.GetStr(Col); #define SERIALIZE_DATE(Var, Col) \ if (Write) \ { \ if (Var.IsValid()) Var.ToUtc(); \ if (!s.SetDate(Col, Var)) \ return false; \ } \ else \ { \ int Fmt = Var.GetFormat(); \ Var.SetFormat(GDTF_YEAR_MONTH_DAY|GDTF_24HOUR); \ Var.Set(s.GetStr(Col)); \ Var.SetFormat(Fmt); \ Var.SetTimeZone(0, false); \ } #define SERIALIZE_BOOL(Var, Col) \ if (Write) \ { \ if (!s.SetInt(Col, Var)) \ return false; \ } \ else \ Var = s.GetBool(Col); #define SERIALIZE_INT(Var, Col) \ if (Write) \ { \ if (!s.SetInt(Col, Var)) \ return false; \ } \ else \ Var = s.GetInt(Col); #define SERIALIZE_INT64(Var, Col) \ if (Write) \ { \ if (!s.SetInt64(Col, Var)) \ return false; \ } \ else \ Var = s.GetInt64(Col); #define SERIALIZE_COLOUR(Colour, Col) \ if (Write) \ { \ int64_t c = Colour.IsValid() ? Colour.c32() : 0; \ if (!s.SetInt64(Col, c)) \ return false; \ } \ else \ { \ int64_t c = s.GetInt64(Col); \ if (c > 0) Colour.c32((uint32_t)c); \ else Colour.Empty(); \ } #define SERIALIZE_AUTOSTR(var, Col) \ { \ if (Write) \ { \ if (!s.SetStr(Col, var)) \ return false; \ } \ else \ { \ var.Reset(NewStr(s.GetStr(Col))); \ } \ } #define SERIALIZE_GSTR(var, Col) \ { \ if (Write) \ { \ if (!s.SetStr(Col, var)) \ return false; \ } \ else \ { \ var = s.GetStr(Col); \ } \ } struct LMail3StoreMsg { enum Type { MsgNone, MsgCompactComplete, MsgRepairComplete, } Msg; int64_t Int; LString Str; LMail3StoreMsg(Type type) { Msg = type; } }; class LMail3Store : public LDataStoreI { friend class LMail3Obj; friend class LMail3Mail; friend class Mail3Trans; friend class CompactThread; LDataEventsI *Callback; sqlite3 *Db; LString Folder; LString DbFile; class LMail3Folder *Root; LHashTbl, GMail3Def*> Fields; LHashTbl, GMail3Idx*> Indexes; Store3Status OpenStatus; LHashTbl, Store3Status> TableStatus; LString ErrorMsg; LString StatusMsg; LString TempPath; std::function CompactOnStatus; std::function RepairOnStatus; struct TableDefn : LArray { LString::Array t; }; bool ParseTableFormat(const char *Name, TableDefn &Defs); Store3Status CheckTable(const char *Name, GMail3Def *Flds); bool UpdateTable(const char *Name, GMail3Def *Flds, Store3Status Check); bool DeleteMailById(int64 Id); bool OpenDb(); bool CloseDb(); LMail3Folder *GetSystemFolder(int Type); public: Mail3SubFormat Format; class LStatement { struct PostBlob { int64 Size; LVariant ColName; LStreamI *Data; }; LArray Post; protected: LMail3Store *Store; sqlite3_stmt *s; LVariant Table; LAutoString TempSql; public: LStatement(LMail3Store *store, const char *sql = 0); virtual ~LStatement(); operator sqlite3_stmt *() { return s; } bool IsOk() { return #ifndef __llvm__ this != 0 && #endif Store != 0 && s != 0; } bool Prepare(const char *Sql); bool Row(); bool Exec(); bool Reset(); bool Finalize(); int64 LastInsertId(); int GetSize(int Col); bool GetBool(int Col); int GetInt(int Col); bool SetInt(int Col, int n); int64 GetInt64(int Col); bool SetInt64(int Col, int64 n); char *GetStr(int Col); bool GetBinary(int Col, LVariant *v); bool SetStr(int Col, char *s); bool SetDate(int Col, LDateTime &d); bool SetStream(int Col, const char *ColName, LStreamI *s); bool SetBinary(int Col, const char *ColName, LVariant *v); virtual int64 GetRowId() { LAssert(0); return -1; } }; class LInsert : public LStatement { public: LInsert(LMail3Store *store, const char *Tbl); int64 GetRowId() { return LastInsertId(); } }; class LUpdate : public LStatement { int64 RowId; public: LUpdate(LMail3Store *store, const char *Tbl, int64 rowId, char *ExcludeField = 0); int64 GetRowId() { return RowId; } }; class LTransaction { LMail3Store *Store; bool Open; public: LTransaction(LMail3Store *store); ~LTransaction(); bool RollBack(); }; #if MAIL3_TRACK_OBJS struct SqliteObjs { LStatement *Stat; Mail3BlobStream *Stream; SqliteObjs() { Stat = 0; Stream = 0; } }; LArray All; template bool RemoveFromAll(T *p) { for (int i=0; i &Items); Store3Status Delete(LArray &Items, bool ToTrash); Store3Status Change(LArray &Items, int PropId, LVariant &Value, LOperator Operator); void Compact(LViewI *Parent, LDataPropI *Props, std::function OnStatus); void Upgrade(LViewI *Parent, LDataPropI *Props, std::function OnStatus); void Repair(LViewI *Parent, LDataPropI *Props, std::function OnStatus); bool SetFormat(LViewI *Parent, LDataPropI *Props); void PostStore(LMail3StoreMsg *m) { Callback->Post(this, m); } void OnEvent(void *Param); bool Check(int Code, const char *Sql); GMail3Def *GetFields(const char *t) { return Fields.Find(t); } StoreTrans StartTransaction(); // LDataEventsI wrappers void OnNew(const char *File, int Line, LDataFolderI *parent, LArray &new_items, int pos, bool is_new); bool OnMove(const char *File, int Line, LDataFolderI *new_parent, LDataFolderI *old_parent, LArray &items); bool OnChange(const char *File, int Line, LArray &items, int FieldHint); bool OnDelete(const char *File, int Line, LDataFolderI *parent, LArray &items); private: class Mail3Trans : public LDataStoreI::LDsTransaction { Mail3Trans **Ptr; LTransaction Trans; public: Mail3Trans(LMail3Store *s, Mail3Trans **ptr); ~Mail3Trans(); } *Transaction; }; class LMail3Obj { protected: bool Check(int r, char *sql); public: LMail3Store *Store; int64 Id; int64 ParentId; LMail3Obj(LMail3Store *store) { Id = ParentId = -1; Store = store; } bool Write(const char *Table, bool Insert); virtual const char *GetClass() { return "LMail3Obj"; } virtual bool Serialize(LMail3Store::LStatement &s, bool Write) = 0; virtual void SetStore(LMail3Store *s) { Store = s; } }; class LMail3Thing : public LDataI, public LMail3Obj { friend class LMail3Store; LMail3Thing &operator =(LMail3Thing &p) = delete; protected: bool NewMail = false; virtual const char *GetTable() { LAssert(0); return 0; } virtual void OnSave() {}; public: LMail3Folder *Parent = NULL; LMail3Thing(LMail3Store *store) : LMail3Obj(store) { } ~LMail3Thing(); const char *GetClass() { return "LMail3Thing"; } bool IsOnDisk() { return Id > 0; } bool IsOrphan() { return false; } uint64 Size() { return sizeof(*this); } uint32_t Type() { LAssert(0); return 0; } Store3Status Delete(bool ToTrash); LDataStoreI *GetStore() { return Store; } LAutoStreamI GetStream(const char *file, int line) { LAssert(0); return LAutoStreamI(0); } bool Serialize(LMail3Store::LStatement &s, bool Write) { LAssert(0); return false; } Store3Status Save(LDataI *Folder = 0); virtual bool DbDelete() { LAssert(0); return false; } }; class LMail3Folder : public LDataFolderI, public LMail3Obj { public: LVariant Name; int Unread; int Open; int ItemType; int Sort; // Which field to sort contents on int Threaded; int SiblingIndex; // The index of this folder when sorting amongst other sibling folders union { int AccessPerms; struct { int16_t ReadPerm; int16_t WritePerm; }; }; Store3SystemFolder System; LMail3Folder *Parent; DIterator Sub; DIterator Items; DIterator Flds; LMail3Folder(LMail3Store *store); ~LMail3Folder(); Store3CopyDecl; bool Serialize(LMail3Store::LStatement &s, bool Write) override; const char *GetClass() override { return "LMail3Folder"; } uint32_t Type() override; bool IsOnDisk() override; bool IsOrphan() override; uint64 Size() override; Store3Status Save(LDataI *Folder) override; Store3Status Delete(bool ToTrash) override; LDataStoreI *GetStore() override; LAutoStreamI GetStream(const char *file, int line) override; bool SetStream(LAutoStreamI stream) override; LDataIterator &SubFolders() override; LDataIterator &Children() override; LDataIterator &Fields() override; Store3Status DeleteAllChildren() override; Store3Status FreeChildren() override; LMail3Folder *FindSub(char *Name); bool DbDelete(); bool GenSizes(); const char *GetStr(int id) override; Store3Status SetStr(int id, const char *str) override; int64 GetInt(int id) override; Store3Status SetInt(int id, int64 i) override; }; class LMail3Attachment : public Store3Attachment { int64 SegId; int64 BlobSize; LAutoString Headers; LString Name; LString MimeType; LString ContentId; LString Charset; /// This is set when the segment is not to be stored on disk. /// When signed and/or encrypted messages are stored, the original /// rfc822 image is maintained by not MIME decoding into separate /// segments but leaving it MIME encoded in one seg (headers and body). /// At runtime the segment is loaded and parsed into a temporary tree /// of LMail3Attachment objects. This flag is set for those temporary /// nodes. bool InMemoryOnly; public: LMail3Attachment(LMail3Store *store); ~LMail3Attachment(); void SetInMemoryOnly(bool b); int64 GetId() { return SegId; } LMail3Attachment *Find(int64 Id); bool Load(LMail3Store::LStatement &s, int64 &ParentId); bool ParseHeaders() override; char *GetHeaders(); bool HasHeaders() { return ValidStr(Headers); } Store3CopyDecl; const char *GetStr(int id) override; Store3Status SetStr(int id, const char *str) override; int64 GetInt(int id) override; Store3Status SetInt(int id, int64 i) override; Store3Status Delete(bool ToTrash = false) override; LAutoStreamI GetStream(const char *file, int line) override; bool SetStream(LAutoStreamI stream) override; uint32_t Type() override { return MAGIC_ATTACHMENT; } bool IsOnDisk() override { return SegId > 0; } bool IsOrphan() override { return false; } uint64 Size() override; uint64 SizeChildren(); Store3Status Save(LDataI *Folder = 0) override; void OnSave() override; }; class LMail3Mail : public LMail3Thing { LAutoString TextCache; LAutoString HtmlCache; LString IdCache; LAutoPtr SizeCache; LString InferredCharset; void LoadSegs(); void OnSave() override; const char *GetTable() override { return MAIL3_TBL_MAIL; } void ParseAddresses(char *Str, int CC); const char *InferCharset(); bool Utf8Check(LVariant &v); bool Utf8Check(LAutoString &v); public: int Priority = MAIL_PRIORITY_NORMAL; int Flags = 0; int AccountId = 0; int64 MailSize = 0; uint32_t MarkColour = Rgba32(0, 0, 0, 0); // FIELD_MARK_COLOUR, 32bit rgba colour LVariant Subject; DIterator To; LMail3Attachment *Seg = NULL; Store3Addr From; Store3Addr Reply; LVariant Label; LVariant MessageID; LVariant References; LVariant FwdMsgId; LVariant BounceMsgId; LVariant ServerUid; LDateTime DateReceived; LDateTime DateSent; LMail3Mail(LMail3Store *store); ~LMail3Mail(); Store3CopyDecl; void SetStore(LMail3Store *s) override; bool Serialize(LMail3Store::LStatement &s, bool Write) override; const char *GetClass() override { return "LMail3Mail"; } LMail3Attachment *GetAttachment(int64 Id); bool FindSegs(const char *MimeType, LArray &Segs, bool Create = false); int GetAttachments(LArray *Lst = 0); bool ParseHeaders() override; void ResetCaches(); uint32_t Type() override; uint64 Size() override; bool DbDelete() override; LDataStoreI *GetStore() override; LAutoStreamI GetStream(const char *file, int line) override; bool SetStream(LAutoStreamI stream) override; const char *GetStr(int id) override; Store3Status SetStr(int id, const char *str) override; int64 GetInt(int id) override; Store3Status SetInt(int id, int64 i) override; const LDateTime *GetDate(int id) override; Store3Status SetDate(int id, const LDateTime *t) override; LDataPropI *GetObj(int id) override; Store3Status SetObj(int id, LDataPropI *i) override; - GDataIt GetList(int id) override; + LDataIt GetList(int id) override; Store3Status SetRfc822(LStreamI *m) override; }; class LMail3Contact : public LMail3Thing { const char *GetTable() override { return MAIL3_TBL_CONTACT; } LHashTbl, LString*> f; public: LVariant Image; LDateTime DateMod; LMail3Contact(LMail3Store *store); ~LMail3Contact(); uint32_t Type() override { return MAGIC_CONTACT; } bool Serialize(LMail3Store::LStatement &s, bool Write) override; Store3CopyDecl; const char *GetClass() override { return "LMail3Contact"; } bool DbDelete() override; const char *GetStr(int id) override; Store3Status SetStr(int id, const char *str) override; int64 GetInt(int id) override { return -1; } LVariant *GetVar(int id) override; Store3Status SetVar(int id, LVariant *i) override; const LDateTime *GetDate(int id) override; Store3Status SetDate(int id, const LDateTime *i) override; }; class LMail3Group : public LMail3Thing { const char *GetTable() override { return MAIL3_TBL_GROUP; } LString Name; LString Group; LDateTime DateMod; public: LMail3Group(LMail3Store *store); ~LMail3Group(); uint32_t Type() override { return MAGIC_GROUP; } bool Serialize(LMail3Store::LStatement &s, bool Write) override; Store3CopyDecl; const char *GetClass() override { return "LMail3Group"; } bool DbDelete() override; const char *GetStr(int id) override; Store3Status SetStr(int id, const char *str) override; int64 GetInt(int id) override { return -1; } const LDateTime *GetDate(int id) override; Store3Status SetDate(int id, const LDateTime *i) override; }; class LMail3Filter : public LMail3Thing { int Index; int StopFiltering; int Direction; LAutoString Name; LAutoString ConditionsXml; LAutoString ActionsXml; LAutoString Script; const char *GetTable() override { return MAIL3_TBL_FILTER; } public: LMail3Filter(LMail3Store *store); ~LMail3Filter(); uint32_t Type() override { return MAGIC_FILTER; } LDataStoreI *GetStore() override { return Store; } bool Serialize(LMail3Store::LStatement &s, bool Write) override; Store3CopyDecl; const char *GetClass() override { return "LMail3Filter"; } bool DbDelete() override; const char *GetStr(int id) override; Store3Status SetStr(int id, const char *str) override; int64 GetInt(int id) override; Store3Status SetInt(int Col, int64 n) override; }; class LMail3Calendar : public LMail3Thing { LString ToCache; private: int CalType; // FIELD_CAL_TYPE LString To; // FIELD_TO CalendarPrivacyType CalPriv; // FIELD_CAL_PRIVACY int Completed; // FIELD_CAL_COMPLETED LDateTime Start; // FIELD_CAL_START_UTC LDateTime End; // FIELD_CAL_END_UTC LString TimeZone; // FIELD_CAL_TIMEZONE LString Subject; // FIELD_CAL_SUBJECT LString Location; // FIELD_CAL_LOCATION LString Uid; // FIELD_UID bool AllDay; // FIELD_CAL_ALL_DAY int64 StoreStatus; // FIELD_STATUS - ie the current Store3Status LString EventStatus; // FIELD_CAL_STATUS LString Reminders; // FIELD_CAL_REMINDERS LDateTime LastCheck; // FIELD_CAL_LAST_CHECK int ShowTimeAs; // FIELD_CAL_SHOW_TIME_AS int Recur; // FIELD_CAL_RECUR int RecurFreq; // FIELD_CAL_RECUR_FREQ int RecurInterval; // FIELD_CAL_RECUR_INTERVAL LDateTime RecurEnd; // FIELD_CAL_RECUR_END_DATE int RecurCount; // FIELD_CAL_RECUR_END_COUNT int RecurEndType; // FIELD_CAL_RECUR_END_TYPE LString RecurPos; // FIELD_CAL_RECUR_FILTER_POS int FilterDays; // FIELD_CAL_RECUR_FILTER_DAYS int FilterMonths; // FIELD_CAL_RECUR_FILTER_MONTHS LString FilterYears; // FIELD_CAL_RECUR_FILTER_YEARS LString Notes; // FIELD_CAL_NOTES LColour Colour; const char *GetTable() override { return MAIL3_TBL_CALENDAR; } public: LMail3Calendar(LMail3Store *store); ~LMail3Calendar(); uint32_t Type() override { return MAGIC_CALENDAR; } LDataStoreI *GetStore() override { return Store; } bool Serialize(LMail3Store::LStatement &s, bool Write) override; Store3CopyDecl; const char *GetClass() override { return "LMail3Filter"; } bool DbDelete() override; const char *GetStr(int id) override; Store3Status SetStr(int id, const char *str) override; int64 GetInt(int id) override; Store3Status SetInt(int Col, int64 n) override; const LDateTime *GetDate(int id) override; Store3Status SetDate(int id, const LDateTime *t) override; }; #endif diff --git a/Code/Store3Mail3/Mail3Mail.cpp b/Code/Store3Mail3/Mail3Mail.cpp --- a/Code/Store3Mail3/Mail3Mail.cpp +++ b/Code/Store3Mail3/Mail3Mail.cpp @@ -1,1637 +1,1637 @@ #include "Mail3.h" #include "lgi/common/NetTools.h" #include "lgi/common/Mail.h" #include "lgi/common/Store3MimeTree.h" #include "lgi/common/StreamConcat.h" #include "lgi/common/TextConvert.h" #include "ScribeUtils.h" GMail3Def TblMail[] = { {"Id", "INTEGER PRIMARY KEY AUTOINCREMENT"}, {"ParentId", "INTEGER"}, // Integers {"Priority", "INTEGER"}, {"Flags", "INTEGER"}, {"AccountId", "INTEGER"}, {"MarkColour", "INTEGER"}, // Strings {"Subject", "TEXT"}, {"ToAddr", "TEXT"}, {"FromAddr", "TEXT"}, {"Reply", "TEXT"}, {"Label", "TEXT"}, {"InternetHeader", "TEXT"}, {"MessageID", "TEXT"}, {"Ref", "TEXT"}, {"FwdMsgId", "TEXT"}, {"BounceMsgId", "TEXT"}, {"ServerUid", "TEXT"}, // Date/times {"DateReceived", "TEXT"}, {"DateSent", "TEXT"}, // Other meta data {"Size", "INTEGER"}, {0, 0} }; GMail3Def TblMailSegs[] = { {"Id", "INTEGER PRIMARY KEY AUTOINCREMENT"}, {"MailId", "INTEGER"}, {"ParentId", "INTEGER"}, {"Headers", "TEXT"}, {"Data", "BLOB"}, {0, 0}, }; bool Mail3_InsertSeg(LMail3Store::LInsert &Ins, LMime *m, int64 ParentId, int64 ParentSeg, int64 &MailSize) { Ins.SetInt64(1, ParentId); Ins.SetInt64(2, ParentSeg); Ins.SetStr(3, m->GetHeaders()); LStreamI *MimeData = m->GetData(); int64 Sz = MimeData ? MimeData->GetSize() : 0; if (Sz > 0) MailSize += Sz; Ins.SetStream(4, "Data", MimeData); if (!Ins.Exec()) return false; ParentSeg = Ins.LastInsertId(); Ins.Reset(); for (int i=0; iLength(); i++) { LMime *c = (*m)[i]; if (!Mail3_InsertSeg(Ins, c, ParentId, ParentSeg, MailSize)) return false; } return true; } bool Mail3_CopySegs(LMail3Mail *m, LMail3Attachment *parent, LDataI *in) { const char *Mt = in->GetStr(FIELD_MIME_TYPE); if (!Mt) { return false; } LMail3Attachment *out = new LMail3Attachment(m->Store); if (!out) return false; out->CopyProps(*in); if (parent) out->AttachTo(parent); else out->AttachTo(m); - GDataIt It = in->GetList(FIELD_MIME_SEG); + LDataIt It = in->GetList(FIELD_MIME_SEG); for (LDataPropI *c = It->First(); c; c = It->Next()) { LDataI *child = dynamic_cast(c); if (child && !Mail3_CopySegs(m, out, child)) return false; } return true; } LMail3Mail::LMail3Mail(LMail3Store *store) : LMail3Thing(store), From(store), Reply(store) { To.State = Store3Loaded; } LMail3Mail::~LMail3Mail() { To.DeleteObjects(); DeleteObj(Seg); } #define DEBUG_COPY_PROPS 0 Store3CopyImpl(LMail3Mail) { #if DEBUG_COPY_PROPS LProfile Prof("Store3CopyProps"); #endif Priority = (int)p.GetInt(FIELD_PRIORITY); Flags = (int)p.GetInt(FIELD_FLAGS); #if DEBUG_COPY_PROPS Prof.Add(_FL); #endif SetStr(FIELD_INTERNET_HEADER, p.GetStr(FIELD_INTERNET_HEADER)); MessageID = p.GetStr(FIELD_MESSAGE_ID); Subject = p.GetStr(FIELD_SUBJECT); MarkColour = (int)p.GetInt(FIELD_COLOUR); AccountId = (int)p.GetInt(FIELD_ACCOUNT_ID); SetStr(FIELD_LABEL, p.GetStr(FIELD_LABEL)); #if DEBUG_COPY_PROPS Prof.Add(_FL); #endif LDataPropI *i = p.GetObj(FIELD_FROM); if (i) From.CopyProps(*i); i = p.GetObj(FIELD_REPLY); if (i) Reply.CopyProps(*i); #if DEBUG_COPY_PROPS Prof.Add(_FL); #endif const LDateTime *d = p.GetDate(FIELD_DATE_RECEIVED); if (d) DateReceived = *d; d = p.GetDate(FIELD_DATE_SENT); if (d) DateSent = *d; #if DEBUG_COPY_PROPS Prof.Add(_FL); #endif To.DeleteObjects(); - GDataIt pTo = p.GetList(FIELD_TO); + LDataIt pTo = p.GetList(FIELD_TO); for (unsigned n=0; nLength(); n++) { To.Insert(new Store3Addr(GetStore(), (*pTo)[n]), -1, true); } To.State = Store3Loaded; #if DEBUG_COPY_PROPS Prof.Add(_FL); #endif LDataI *Root = dynamic_cast(p.GetObj(FIELD_MIME_SEG)); if (Root) { // Must commit ourselves now and get an Id if needed #if DEBUG_COPY_PROPS Prof.Add(_FL); #endif if (Id > 0 || Write(MAIL3_TBL_MAIL, true)) { // Clear existing segment if present... LAssert(!Seg || Seg->GetId() < 0); DeleteObj(Seg); #if DEBUG_COPY_PROPS Prof.Add(_FL); #endif // Save all the segments out Mail3_CopySegs(this, NULL, Root); #if DEBUG_COPY_PROPS Prof.Add(_FL); #endif OnSave(); } } return true; } void LMail3Mail::SetStore(LMail3Store *s) { if (Id < 0) { LMail3Thing::SetStore(s); From.SetStore(s); Reply.SetStore(s); if (Seg) Seg->SetMail(this); } else LAssert(!"Object is already commited to another store."); } bool SafeAddrTokenize(const char *In, List &Out, bool Debug = false) { if (!In) return false; // Find all the characters of interest... LArray a; for (const char *c = In; *c; c++) { if (strchr("\':,<@>", *c)) a.Add(c); } // Now do pattern matching. We may encounter weird numbers of single // quotes due to recipient names having un-escaped single quotes. const char *Last = In; LAutoString Str; for (ssize_t i=0; i<(ssize_t)a.Length(); i++) { // Check for '<' '@', '>' pattern if (i < (ssize_t)a.Length() - 4 && *a[i] == '<' && *a[i+1] == '@' && *a[i+2] == '>' && *a[i+3] == ':' && *a[i+4] == ',') { const char *e = a[i+4]; Str.Reset(NewStr(Last, e-Last)); Last = e + 1; } else if (i < (ssize_t)a.Length() - 3 && *a[i] == '<' && *a[i+1] == '@' && *a[i+2] == '>' && *a[i+3] == ':') { const char *e = a[i+3]; while (*e) e++; Str.Reset(NewStr(Last, e-Last)); Last = e; } else if (i < (ssize_t)a.Length() - 2 && *a[i] == '<' && *a[i+1] == '>' && *a[i+2] == ':') { // This case handles a group name without a '@' in it... const char *e = a[i+2]; while (*e) e++; Str.Reset(NewStr(Last, e-Last)); Last = e; } else break; if (Str) { Out.Insert(Str.Release()); } } return true; } #if 1 #define DEBUG_SERIALIZE(...) if (Debug) LgiTrace(__VA_ARGS__) #else #define DEBUG_SERIALIZE(...) #endif bool LMail3Mail::Serialize(LMail3Store::LStatement &s, bool Write) { static LVariant vTo, vFrom, vReply; bool Debug = false; // Save the objects to strings if (Write) { if (MailSize < 0 && Seg) { MailSize = (uint32_t) (Seg->Size() + Seg->SizeChildren()); } if (!From.GetValue("Text", vFrom)) vFrom.Empty(); if (!Reply.GetValue("Text", vReply)) vReply.Empty(); LStringPipe p; int Done = 0; for (unsigned i=0; iGetValue("Text", v) && v.Str()) { const char *Comma = Done ? ", " : ""; DEBUG_SERIALIZE("ToText='%s'\n", v.Str()); p.Print("%s%s:%i", Comma, v.Str(), a->CC); Done++; } } vTo.OwnStr(p.NewStr()); DEBUG_SERIALIZE("vTo='%s'\n", vTo.Str()); } int i = 0; LVariant NullInternetHeader; // The internet header is now stored // In the first segment ONLY. But it // wasn't worth removing the field in // the DB. SERIALIZE_INT64(Id, i++); SERIALIZE_INT64(ParentId, i++); SERIALIZE_INT(Priority, i++); SERIALIZE_INT(Flags, i++); SERIALIZE_INT(AccountId, i++); SERIALIZE_INT(MarkColour, i++); /* auto sub = Subject.Str(); if (sub) { uint32_t w; for (LUtf8Ptr utf(sub); w = utf; utf++) { LgiTrace("%i 0x%x\n", w, w); } } */ SERIALIZE_STR(Subject, i++); SERIALIZE_STR(vTo, i++); SERIALIZE_STR(vFrom, i++); SERIALIZE_STR(vReply, i++); SERIALIZE_STR(Label, i++); SERIALIZE_STR(NullInternetHeader, i++); SERIALIZE_STR(MessageID, i++); SERIALIZE_STR(References, i++); SERIALIZE_STR(FwdMsgId, i++); SERIALIZE_STR(BounceMsgId, i++); SERIALIZE_STR(ServerUid, i++); SERIALIZE_DATE(DateReceived, i++); SERIALIZE_DATE(DateSent, i++); SERIALIZE_INT64(MailSize, i++); // Load the objects from the strings if (!Write) { From.SetValue("Text", vFrom); Reply.SetValue("Text", vReply); To.DeleteObjects(); List Addr; char *ToStr = vTo.Str(); //DEBUG_SERIALIZE("ReadTo='%s'\n", ToStr); int Colons = 0; int Commas = 0; for (char *c = ToStr; c && *c; c++) { if (*c == ':') Colons++; else if (*c == ',') Commas++; } if (Commas == 1 && Colons > 1) { // Broken data char *c = ToStr; while (*c) { while (*c && (strchr(WhiteSpace, *c) || *c == ',')) c++; char *e = strchr(c, '>'); if (!e) break; e++; if (*e == ':') e++; while (IsDigit(*e)) e++; char *a = NewStr(c, e - c); if (!a) break; Addr.Insert(a); c = e; } } else { // Correct data? Hopefully... #if 1 SafeAddrTokenize(vTo.Str(), Addr); #else TokeniseStrList(vTo.Str(), Addr, ","); #endif } for (auto r: Addr) { Store3Addr *a = new Store3Addr(GetStore()); if (a) { char *CcType = strrchr(r, ':'); if (CcType) *CcType++ = 0; LAutoString Name, Addr; DecodeAddrName(r, Name, Addr, 0); a->SetStr(FIELD_NAME, Name); a->SetStr(FIELD_EMAIL, Addr); a->CC = CcType ? atoi(CcType) : 0; To.Insert(a, -1, true); } } Addr.DeleteArrays(); To.State = Store3Loaded; } return true; } uint32_t LMail3Mail::Type() { return MAGIC_MAIL; } size_t Sizeof(LVariant &v) { int s = 0; switch (v.Type) { default: break; case GV_STRING: return (int)strlen(v.Str()); case GV_WSTRING: return StrlenW(v.WStr()) * sizeof(char16); } return s + sizeof(v); } size_t Sizeof(DIterator &i) { size_t s = sizeof(i); for (unsigned n=0; nSizeof(); return s; } uint64 LMail3Mail::Size() { if (MailSize < 0 && Seg) { MailSize = (uint32_t) (Seg->Size() + Seg->SizeChildren()); } return MailSize + // Size of mime segments... sizeof(Priority) + sizeof(Flags) + sizeof(AccountId) + Sizeof(Subject) + Sizeof(To) + From.Sizeof() + Reply.Sizeof() + Sizeof(Label) + // Sizeof(InternetHeader) + Sizeof(MessageID) + Sizeof(References) + Sizeof(FwdMsgId) + Sizeof(BounceMsgId) + Sizeof(ServerUid) + sizeof(DateReceived) + sizeof(DateSent); } bool LMail3Mail::DbDelete() { return Store->DeleteMailById(Id); } LDataStoreI *LMail3Mail::GetStore() { return Store; } LAutoStreamI LMail3Mail::GetStream(const char *file, int line) { LAutoStreamI Ret; const char *TmpPath = Store->GetStr(FIELD_TEMP_PATH); if (Flags & MAIL_STORED_FLAT) { // Construct from the single segment... LString Sql; Sql.Printf("select * from '%s' where MailId=" LPrintfInt64, MAIL3_TBL_MAILSEGS, Id); LMail3Store::LStatement s(Store, Sql); if (s.Row()) { LStreamConcat *Sc; int64 SegId = s.GetInt64(0); char *Hdr = s.GetStr(3); if (Hdr && Ret.Reset(Sc = new LStreamConcat)) { size_t HdrLen = strlen(Hdr); int BlobSize = s.GetSize(4); Sc->Add(new LMemStream(Hdr, HdrLen)); Sc->Add(new LMemStream("\r\n\r\n", 4)); Sc->Add(new Mail3BlobStream(Store, (int)SegId, BlobSize, file, line)); LFile f; if (f.Open("c:\\temp\\email.eml", O_WRITE)) { LCopyStreamer Cp(64<<10); Cp.Copy(Sc, &f); f.Close(); Sc->SetPos(0); } } } } else if (Ret.Reset(new LTempStream(TmpPath))) { // Encode from multiple segments... LoadSegs(); if (Seg) { LMime Mime(TmpPath); if (Store3ToGMime(&Mime, Seg)) { if (!Mime.Text.Encode.Push(Ret)) { LgiTrace("%s:%i - Mime encode failed.\n", _FL); Ret.Reset(); } } else { LgiTrace("%s:%i - Store3ToGMime failed.\n", _FL); Ret.Reset(); } } else { LgiTrace("%s:%i - No segment to encode.\n", _FL); Ret.Reset(); } } return Ret; } bool LMail3Mail::SetStream(LAutoStreamI stream) { if (!stream) return false; DeleteObj(Seg); // MIME parse the stream and store it to segments. LMime Mime; if (Mime.Text.Decode.Pull(stream)) { Seg = new LMail3Attachment(Store); if (Seg) { // This stops the objects being written to disk. // Which would mean we have multiple copies of the same // data on disk. This setting also propagates down the // tree automatically as GMimeToStore3 saves new child // notes to their parents. Seg->SetInMemoryOnly(true); Seg->AttachTo(this); GMimeToStore3(Seg, &Mime); } } return false; } bool LMail3Mail::FindSegs(const char *MimeType, LArray &Results, bool Create) { LoadSegs(); if ((!Seg || !Seg->FindSegs(MimeType, Results)) && Create) { LMail3Attachment *a = new LMail3Attachment(Store); if (!a) return false; a->SetStr(FIELD_MIME_TYPE, MimeType); Store3MimeTree Tree(this, Seg); Tree.Add(a); if (!Tree.Build()) return false; Results.Add(a); } /* for (unsigned i=0; iGetStr(FIELD_MIME_TYPE)) { // Ugh, a bug has caused a bunch of NULL mime-types.. a->SetStr(FIELD_MIME_TYPE, MimeType); } } */ return Results.Length() > 0; } LMail3Attachment *LMail3Mail::GetAttachment(int64 Id) { return Seg ? Seg->Find(Id) : 0; } struct Pair { int64 Parent; LMail3Attachment *Seg; }; int LMail3Mail::GetAttachments(LArray *Lst) { if (!Seg) return -1; int Count = 0; if (Seg->IsMultipart()) { - GDataIt It = Seg->GetList(FIELD_MIME_SEG); + LDataIt It = Seg->GetList(FIELD_MIME_SEG); if (It) { for (LDataPropI *i=It->First(); i; i=It->Next()) { const char *Mt = i->GetStr(FIELD_MIME_TYPE); const char *Name = i->GetStr(FIELD_NAME); if ( ( Mt && _stricmp("text/plain", Mt) && _stricmp("text/html", Mt) ) || ValidStr(Name) ) { Count++; if (Lst) { LMail3Attachment *a = dynamic_cast(i); if (a) Lst->Add(a); } } } } } return Count; } void LMail3Mail::LoadSegs() { if (Id > 0 && !Seg) { if (Flags & MAIL_STORED_FLAT) { // Decompression flat MIME storage to tree // GetStream will do the hard work of joining the message back // into RFC822 format. LAutoStreamI Rfc822 = GetStream(_FL); if (!Rfc822) return; // Then we MIME parse it and store it to segments. SetStream(Rfc822); } else { LArray Others; char Sql[256]; sprintf_s(Sql, sizeof(Sql), "select * from '%s' where MailId=" LPrintfInt64, MAIL3_TBL_MAILSEGS, Id); LMail3Store::LStatement s(Store, Sql); while (s.Row()) { Pair p; p.Seg = new LMail3Attachment(Store); if (p.Seg->Load(s, p.Parent)) { if (p.Parent <= 0) { if (Seg) { // hmmm, this shouldn't happen p.Seg->AttachTo(Seg); } else { p.Seg->AttachTo(this); } } else { Others.Add(p); } } } while (Seg && Others.Length()) { ssize_t StartSize = Others.Length(); for (unsigned i=0; iFind(Others[i].Parent); if (p) { Others[i].Seg->AttachTo(p); Others.DeleteAt(i--); } } if (StartSize == Others.Length()) { // No segments could be attached? Must have borked up parent id's while (Others.Length()) { // Just attach them somewhere... anywhere...! if (Seg) Others[0].Seg->AttachTo(Seg); else Others[0].Seg->AttachTo(this); Others.DeleteAt(0); } } } if (Seg) { int Attached = GetAttachments(); int NewFlags; if (Attached > 0) NewFlags = Flags | MAIL_ATTACHMENTS; else NewFlags = Flags & ~MAIL_ATTACHMENTS; if (NewFlags != Flags) { Flags = NewFlags; Save(); } } } } } static auto DefaultCharset = "windows-1252"; const char *LMail3Mail::GetStr(int id) { switch (id) { case FIELD_DEBUG: { static char s[64]; sprintf_s(s, sizeof(s), "Mail3.Id=" LPrintfInt64, Id); return s; } case FIELD_SUBJECT: { if (!LIsUtf8(Subject.Str())) { auto cs = DetectCharset(Subject.Str()); LAutoString conv; if (conv.Reset((char*)LNewConvertCp("utf-8", Subject.Str(), cs ? cs.Get() : DefaultCharset))) Subject = conv; } #if 0 int32 ch; LgiTrace("Subj:"); for (LUtf8Ptr p(Subject.Str()); ch = p; p++) LgiTrace(" u+%x", ch); LgiTrace("\n"); #endif return Subject.Str(); } case FIELD_CHARSET: { return "utf-8"; } case FIELD_TEXT: { LArray Results; if (!TextCache && FindSegs("text/plain", Results)) { LStringPipe p; for (auto seg: Results) { if (seg->GetStr(FIELD_NAME)) continue; auto s = seg->GetStream(_FL); if (!s) continue; LString Charset = seg->GetStr(FIELD_CHARSET); LAutoString Buf; auto Size = s->GetSize(); if (Size <= 0) continue; Buf.Reset(new char[Size+1]); s->Read(&Buf[0], Size); Buf[Size] = 0; if (!Charset) Charset = DetectCharset(Buf.Get()); if (!Charset) Charset = DefaultCharset; LAutoString Utf8; if (!Stricmp(Charset.Get(), "utf-8") && LIsUtf8(Buf)) Utf8 = Buf; else Utf8.Reset((char*)LNewConvertCp("utf-8", Buf, Charset, Size)); if (Results.Length() > 1) { if (p.GetSize()) p.Print("\n---------------------------------------------\n"); p.Push(Utf8); } else { TextCache = Utf8; return TextCache; } } TextCache.Reset(p.NewStr()); } return TextCache; } case FIELD_HTML_CHARSET: { return "utf-8"; } case FIELD_ALTERNATE_HTML: { LArray Results; if (!HtmlCache && FindSegs("text/html", Results)) { LMemQueue Blocks(1024); int64 Total = 0; for (unsigned i=0; iGetStr(FIELD_NAME)) { LAutoStreamI s = Results[i]->GetStream(_FL); if (s) { int64 Size = s->GetSize(); if (Size > 0) { auto Charset = Results[i]->GetStr(FIELD_CHARSET); char *Buf = new char[(size_t)Size+1]; ssize_t Rd = s->Read(Buf, (int)Size); if (Rd > 0) { Buf[Rd] = 0; if (Charset && _stricmp("utf-8", Charset) != 0) { char *Tmp = (char*)LNewConvertCp("utf-8", Buf, Charset, (int)Size); if (Tmp) { DeleteArray(Buf); Buf = Tmp; Size = strlen(Tmp); } } Blocks.Write(Buf, (int)Size); Total += Size; DeleteArray(Buf); } } } } } HtmlCache.Reset((char*)Blocks.New(1)); } return HtmlCache; } case FIELD_LABEL: return Label.Str(); case FIELD_INTERNET_HEADER: { auto Root = GetObj(FIELD_MIME_SEG); if (Root) return Root->GetStr(id); else return NULL; } case FIELD_REFERENCES: return References.Str(); case FIELD_FWD_MSG_ID: return FwdMsgId.Str(); case FIELD_BOUNCE_MSG_ID: return BounceMsgId.Str(); case FIELD_SERVER_UID: return ServerUid.Str(); case FIELD_MESSAGE_ID: { if (!MessageID.Str()) { LAutoString Header(InetGetHeaderField(GetStr(FIELD_INTERNET_HEADER), "Message-ID")); if (Header) { List Ids; ParseIdList(Header, Ids); MessageID = Ids[0]; Ids.DeleteArrays(); } } return MessageID.Str(); } case FIELD_UID: { IdCache.Printf(LPrintfInt64, Id); return IdCache; } } LAssert(0); return 0; } void LMail3Mail::OnSave() { if (Seg) { LDataStoreI::StoreTrans Trans = Store->StartTransaction(); Seg->OnSave(); } } void LMail3Mail::ParseAddresses(char *Str, int CC) { List Addr; TokeniseStrList(Str, Addr, ","); for (auto RawAddr: Addr) { LAutoPtr a(To.Create(Store)); if (a) { Store3Addr *sa = dynamic_cast(a.Get()); LAssert(sa != NULL); if (sa) { DecodeAddrName(RawAddr, sa->Name, sa->Addr, 0); sa->CC = CC; To.Insert(a.Release()); } } } Addr.DeleteArrays(); } void LMail3Mail::ResetCaches() { TextCache.Reset(); HtmlCache.Reset(); SizeCache.Reset(); } const char *LMail3Mail::InferCharset() { if (!InferredCharset) { // Sometimes mailers don't follow the rules... *cough*outlook*cough* // So lets play the "guess the charset" game... // Maybe one of the segments has a charset? InferredCharset = Seg->GetStr(FIELD_CHARSET); if (!InferredCharset) { // What about 'Content-Language'? auto InetHdrs = GetStr(FIELD_INTERNET_HEADER); if (InetHdrs) { LAutoString ContentLang(InetGetHeaderField(InetHdrs, "Content-Language")); if (ContentLang) { LLanguage *Lang = LFindLang(ContentLang); if (Lang) InferredCharset = Lang->Charset; } } } } return InferredCharset; } bool LMail3Mail::Utf8Check(LAutoString &v) { if (!LIsUtf8(v.Get())) { const char *Cs = InferCharset(); if (Cs) { LAutoString Value((char*) LNewConvertCp("utf-8", v, Cs, -1)); if (Value) { v = Value; return true; } } } return false; } bool LMail3Mail::Utf8Check(LVariant &v) { if (!LIsUtf8(v.Str())) { const char *Cs = InferCharset(); if (Cs) { LAutoString Value((char*) LNewConvertCp("utf-8", v.Str(), Cs, -1)); if (Value) { v.OwnStr(Value.Release()); return true; } } } return false; } bool LMail3Mail::ParseHeaders() { // Reload from headers... auto InetHdrs = GetStr(FIELD_INTERNET_HEADER); Subject.OwnStr(DecodeRfc2047(InetGetHeaderField(InetHdrs, "subject"))); Utf8Check(Subject); // From LAutoString s(DecodeRfc2047(InetGetHeaderField(InetHdrs, "from"))); Utf8Check(s); From.Empty(); DecodeAddrName(s, From.Name, From.Addr, NULL); s.Reset(DecodeRfc2047(InetGetHeaderField(InetHdrs, "reply-to"))); Utf8Check(s); Reply.Empty(); DecodeAddrName(s, Reply.Name, Reply.Addr, NULL); // Parse To and CC headers. To.DeleteObjects(); if (s.Reset(DecodeRfc2047(InetGetHeaderField(InetHdrs, "to")))) { Utf8Check(s); ParseAddresses(s, MAIL_ADDR_TO); } if (s.Reset(DecodeRfc2047(InetGetHeaderField(InetHdrs, "cc")))) { Utf8Check(s); ParseAddresses(s, MAIL_ADDR_CC); } // Data if (s.Reset(InetGetHeaderField(InetHdrs, "date"))) { DateSent.Decode(s); DateSent.ToUtc(); } DeleteObj(Seg); ResetCaches(); return true; } Store3Status LMail3Mail::SetStr(int id, const char *str) { switch (id) { case FIELD_SUBJECT: Subject = str; break; case FIELD_TEXT: { TextCache.Reset(); LArray Results; if (FindSegs("text/plain", Results, str != 0)) { for (unsigned i=0; iGetStr(FIELD_NAME)) { LAutoStreamI s(str ? new LMemStream((char*)str, strlen(str)) : 0); Results[i]->SetStream(s); break; } } } break; } case FIELD_CHARSET: { LArray Results; if (FindSegs("text/plain", Results, str != 0)) { for (unsigned i=0; iGetStr(FIELD_NAME)) { Results[i]->SetStr(FIELD_CHARSET, str); break; } } } break; } case FIELD_ALTERNATE_HTML: { HtmlCache.Reset(); LArray Results; if (FindSegs("text/html", Results, str != 0)) { for (unsigned i=0; iGetStr(FIELD_NAME)) { if (str) { LAutoStreamI tmp(new LMemStream((char*)str, strlen(str))); Results[i]->SetStream(tmp); } else { auto s = Results[i]; s->Delete(); delete s; } break; } } } break; } case FIELD_HTML_CHARSET: { LArray Results; if (FindSegs("text/html", Results, str != 0)) { const char *Charset = str && *str == '>' ? str + 1 : str; for (unsigned i=0; iGetStr(FIELD_NAME)) { if (Results[i]->SetStr(FIELD_CHARSET, Charset)) return Store3Success; break; } } } return Store3Error; } case FIELD_LABEL: Label = str; break; case FIELD_INTERNET_HEADER: { LoadSegs(); if (!Seg) { // This happens when the user re-sends an email and it creates // a new empty email to copy the old sent email into. LMail3Attachment *a = new LMail3Attachment(Store); if (!a) { LAssert(0); return Store3Error; } a->AttachTo(this); } if (!Seg) { LAssert(0); return Store3Error; } Seg->SetStr(id, str); break; } case FIELD_MESSAGE_ID: MessageID = str; break; case FIELD_REFERENCES: References = str; break; case FIELD_FWD_MSG_ID: FwdMsgId = str; break; case FIELD_BOUNCE_MSG_ID: BounceMsgId = str; break; case FIELD_SERVER_UID: ServerUid = str; break; default: LAssert(0); return Store3Error; } return Store3Success; } int64 LMail3Mail::GetInt(int id) { switch (id) { case FIELD_STORE_TYPE: return Store3Sqlite; case FIELD_SIZE: return Size(); case FIELD_LOADED: return Store3Loaded; case FIELD_PRIORITY: return Priority; case FIELD_FLAGS: return Flags; case FIELD_DONT_SHOW_PREVIEW: return false; case FIELD_ACCOUNT_ID: return AccountId; case FIELD_COLOUR: return (uint64_t)MarkColour; case FIELD_SERVER_UID: return -1; } LAssert(0); return -1; } Store3Status LMail3Mail::SetInt(int id, int64 i) { switch (id) { case FIELD_LOADED: return Store3Success; case FIELD_PRIORITY: Priority = (int)i; return Store3Success; case FIELD_FLAGS: Flags = (int)i; return Store3Success; case FIELD_ACCOUNT_ID: AccountId = (int)i; return Store3Success; case FIELD_COLOUR: if (i < 0) MarkColour = Rgba32(0, 0, 0, 0); // transparent else MarkColour = (uint32_t)i; return Store3Success; } LAssert(0); return Store3NotImpl; } const LDateTime *LMail3Mail::GetDate(int id) { switch (id) { case FIELD_DATE_RECEIVED: return &DateReceived; case FIELD_DATE_SENT: return &DateSent; } LAssert(0); return 0; } Store3Status LMail3Mail::SetDate(int id, const LDateTime *t) { switch (id) { case FIELD_DATE_RECEIVED: if (t) DateReceived = *t; else DateReceived.Year(0); return Store3Success; case FIELD_DATE_SENT: if (t) DateSent = *t; else DateSent.Year(0); return Store3Success; } LAssert(0); return Store3NotImpl; } LDataPropI *LMail3Mail::GetObj(int id) { switch (id) { case FIELD_FROM: return &From; case FIELD_REPLY: return &Reply; case FIELD_MIME_SEG: { LoadSegs(); /* This causes replies to have the wrong format for "text/plain" if (!Seg) { LMail3Attachment *a = new LMail3Attachment(Store); if (a) { a->SetStr(FIELD_MIME_TYPE, sMultipartMixed); a->AttachTo(this); } } */ return Seg; } } LAssert(0); return 0; } Store3Status LMail3Mail::SetObj(int id, LDataPropI *i) { switch (id) { case FIELD_MIME_SEG: { if (Seg) { Seg->SetMail(NULL); Seg = NULL; } LMail3Attachment *a = dynamic_cast(i); if (!a) { LAssert(!"Incorrect object..."); return Store3Error; } Seg = a; Seg->SetMail(this); break; } case FIELD_HTML_RELATED: { LMail3Attachment *a = i ? dynamic_cast(i) : NULL; LoadSegs(); if (Seg) { Store3MimeTree Tree(this, Seg); if (a) Tree.MsgHtmlRelated.Add(a); else Tree.MsgHtmlRelated.Empty(); if (!Tree.Build()) return Store3Error; MailSize = -1; Seg->Save(this); } break; } default: LAssert(0); return Store3NotImpl; } return Store3Success; } -GDataIt LMail3Mail::GetList(int id) +LDataIt LMail3Mail::GetList(int id) { switch (id) { case FIELD_TO: return &To; } LAssert(0); return 0; } class LSubStream : public LStreamI { // The source stream to read from LStreamI *s; // The start position in the source stream int64 Start; // The length of the sub-stream int64 Len; // The current position in the sub-stream // (relative to the start of the sub-stream, not the parent stream) int64 Pos; public: LSubStream(LStreamI *stream, int64 start = 0, int64 len = -1) { s = stream; Pos = 0; Start = MAX(0, start); int64 MaxLen = s->GetSize() - Start; if (MaxLen < 0) MaxLen = 0; Len = len < 0 && s ? MaxLen : MIN(MaxLen, len); } bool IsOpen() { return true; } int Close() { s = NULL; Start = Len = Pos = 0; return true; } int64 GetSize() { return s ? Len : -1; } int64 SetSize(int64 Size) { // Can't set size return GetSize(); } int64 GetPos() { return s ? Pos : -1; } int64 SetPos(int64 p) { if (p < 0) p = 0; if (p >= Len) p = Len; return Pos = p; } ssize_t Read(void *Buffer, ssize_t Size, int Flags = 0) { ssize_t r = 0; if (s && Buffer) { int64 Remaining = Len - Pos; ssize_t Common = MIN(Size, (int)Remaining); int64 SrcPos = Start + Pos; int64 ActualPos = s->SetPos(SrcPos); if (ActualPos == SrcPos) { r = s->Read(Buffer, Common, Flags); if (r > 0) { Pos += r; } } else LAssert(0); } return r; } ssize_t Write(const void *Buffer, ssize_t Size, int Flags = 0) { LAssert(!"Not implemented."); return 0; } LStreamI *Clone() { return new LSubStream(s, Start, Len); } }; Store3Status LMail3Mail::SetRfc822(LStreamI *m) { Store3Status Status = Store3Error; if (m) { // Save all the segments out LDataStoreI::StoreTrans Trans = Store->StartTransaction(); if (Id < 0) { // Must commit ourselves now and get an Id if (!Write(MAIL3_TBL_MAIL, true)) return Store3Error; } else { char s[256]; sprintf_s(s, sizeof(s), "delete from %s where MailId=" LPrintfInt64, MAIL3_TBL_MAILSEGS, Id); LMail3Store::LStatement Del(Store, s); if (!Del.Exec()) return Store3Error; } DeleteObj(Seg); /* Normally the message is parsed into MIME segments and stored parsed and decoded into the MailSegs table. If the message is encrypted or signed that could the client can't verify the message later because the specifics of MIME encoding varies between clients. So if the message is: Signed and/or Encrypted: There is only one MAILSEG record with all the headers of the root MIME segment, and the body stream has all the data of the RFC822 image. Otherwise: Normal MIME parsing is done, storing the message in different MAILSEG records. Which was the previous behaviour. First the headers of the root MIME node are read to see what the Content-Type is. Then a decision about how to store the node is made. */ LString Hdrs = HeadersFromStream(m); LAutoString Type(InetGetHeaderField(Hdrs, "Content-Type")); Flags &= ~MAIL_STORED_FLAT; if (Type) { LString s = Type.Get(); ptrdiff_t Colon = s.Find(";"); if (Colon > 0) s.Length((uint32_t)Colon); s = s.Strip().Lower(); if (s == sMultipartEncrypted || s == sMultipartSigned) { Flags |= MAIL_STORED_FLAT; } } LMail3Store::LInsert Ins(Store, MAIL3_TBL_MAILSEGS); MailSize = 0; LMime Mime; if (Flags & MAIL_STORED_FLAT) { // Don't parse MIME into a tree. Mime.SetHeaders(Hdrs); Mime.SetData(true, new LSubStream(m, Hdrs.Length()+4)); if (Mail3_InsertSeg(Ins, &Mime, Id, -1, MailSize)) Status = Store3Success; } else { // Do normal parsing in to MIME tree. if (Mime.Text.Decode.Pull(m) && Mail3_InsertSeg(Ins, &Mime, Id, -1, MailSize)) Status = Store3Success; } } return Status; } diff --git a/Code/Store3Mapi/ScribeMapi.h b/Code/Store3Mapi/ScribeMapi.h --- a/Code/Store3Mapi/ScribeMapi.h +++ b/Code/Store3Mapi/ScribeMapi.h @@ -1,822 +1,822 @@ #ifndef _SCRIBE_MAPI_H_ #define _SCRIBE_MAPI_H_ #ifdef WINDOWS #include #define INITGUID #define USES_IID_IMessage #include "lgi/common/Lgi.h" #include "Store3Common.h" #include "lgi/common/Store3.h" #include "mapix.h" // https://www.microsoft.com/en-us/download/details.aspx?id=12905 #include "mapiutil.h" #if _MSC_VER >= 1400 typedef ULONG_PTR UI_TYPE; #else typedef ULONG UI_TYPE; #endif typedef HRESULT (STDAPICALLTYPE *pWrapCompressedRTFStream) ( LPSTREAM lpCompressedRTFStream, ULONG ulFlags, LPSTREAM FAR *lpUncompressedRTFStream ); #define PR_SMTP_ADDRESS (PROP_TAG(PT_STRING8, 0x39fe)) #define PR_SENDER_SMTP_ADDRESS (PROP_TAG(PT_STRING8, 0x0065)) #define PR_SENDER_SMTP_ADDRESS2 (PROP_TAG(PT_STRING8, 0x0c1f)) #ifndef PR_INTERNET_MESSAGE_ID #define PR_INTERNET_MESSAGE_ID 0x1035001E #endif #ifndef PR_HTML #define PR_HTML 0x10130102 #endif #ifndef PR_BODY_HTML #define PR_BODY_HTML 0x1013001E #endif #ifndef PR_ATTACH_CONTENT_ID #define PR_ATTACH_CONTENT_ID 0x3712001E #endif #ifndef PR_INTERNET_CPID #define PR_INTERNET_CPID 0x3FDE0003 #endif #ifndef PR_ATTACH_CONTENT_ID_W #define PR_ATTACH_CONTENT_ID_W 0x3712001F #endif #ifndef PR_LAST_MODIFIER_NAME #define PR_LAST_MODIFIER_NAME 0x3FFA001F #endif extern uint32_t MapiContactEmailTags[]; class LMapiStore; class LMapiFolder; class LMapiMail; class ScribeMapiList; class LMapiAdviseSink; class LMapiBase { public: SPropValue *MapiGetField(SRow *Row, int Field) { if (!Row) return NULL; for (unsigned i=0; icValues; i++) { if (PROP_ID(Row->lpProps[i].ulPropTag) == PROP_ID(Field)) { return Row->lpProps + i; } } return NULL; } SPropValue *MapiGetProp(IMAPIProp *Props, int Field) { if (!Props) return NULL; SPropTagArray InTag; InTag.cValues = 1; InTag.aulPropTag[0] = Field; SPropValue *OutTag = 0; ULONG Tags = 0; if (Props) { HRESULT res = Props->GetProps(&InTag, 0, &Tags, &OutTag); if (SUCCEEDED(res)) { if (Tags == 1) { return OutTag; } } } return NULL; } int64 MapiCastInt(SPropValue *Val) { if (!Val) return 0; switch (PROP_TYPE(Val->ulPropTag)) { case PT_SHORT: return Val->Value.i; case PT_LONG: return Val->Value.l; case PT_I8: return Val->Value.li.QuadPart; } return 0; } LString MapiCastString(SPropValue *Val) { if (!Val) return LString(); switch (PROP_TYPE(Val->ulPropTag)) { case PT_STRING8: { return LString(Val->Value.lpszA); } case PT_UNICODE: { LAutoString u(WideToUtf8(Val->Value.lpszW)); return LString(u.Get()); } case PT_BINARY: { LString s((const char*)Val->Value.bin.lpb, Val->Value.bin.cb); return s; } } return LString(); } bool MapiCastBinary(SPropValue *Val, void *&Ptr, int &Size) { if (Val && PROP_TYPE(Val->ulPropTag) == PT_BINARY) { Ptr = Val->Value.bin.lpb; Size = Val->Value.bin.cb; return true; } return false; } bool MapiCastDate(LDateTime &dt, SPropValue *Val) { if (!Val) return false; if (PROP_TYPE(Val->ulPropTag) != PT_SYSTIME) return false; dt.Set((uint64)Val->Value.ft.dwHighDateTime << 32 | Val->Value.ft.dwLowDateTime); return true; } LString MapiGetPropStr(IMAPIProp *Props, int Field) { SPropValue *Val = MapiGetProp(Props, Field); if (Val) { return MapiCastString(Val); } return LString(); } int64 MapiGetPropInt(IMAPIProp *Props, int Field) { SPropValue *Val = MapiGetProp(Props, Field); if (Val) { return MapiCastInt(Val); } return 0; } bool MapiGetPropDate(LDateTime &dt, IMAPIProp *Props, int Field) { if (!Props) return false; SPropValue *p = MapiGetProp(Props, Field); if (!p) return false; return MapiCastDate(dt, p); } bool MapiSetPropStr(IMAPIProp *Props, int Field, const char *Str, bool Unicode = false) { if (Props && Str) { LAssert(PROP_TYPE(Field) == PT_STRING8); SPropValue p; LString n; p.ulPropTag = Field; if (Unicode) p.Value.lpszA = n = LToNativeCp(Str); else p.Value.lpszA = (LPSTR)Str; HRESULT res = Props->SetProps(1, &p, 0); return SUCCEEDED(res); } return false; } bool MapiSetPropLong(IMAPIProp *Props, int Field, ULONG lng) { if (!Props) return false; SPropValue p; p.ulPropTag = Field; p.Value.l = lng; HRESULT res = Props->SetProps(1, &p, 0); if (SUCCEEDED(res)) return true; LgiTrace("%s:%i - Failed to set prop %i\n", _FL, Field); return false; } bool MapiSetPropBool(IMAPIProp *Props, int Field, bool b) { if (Props) { SPropValue p; p.ulPropTag = Field; p.Value.b = b; HRESULT res = Props->SetProps(1, &p, 0); return SUCCEEDED(res); } return false; } bool MapiSetPropDate(IMAPIProp *Props, int Field, const LDateTime &d, bool AdjustTz = true) { if (d.Year()) { // Set sent date SPropValue Prop; Prop.ulPropTag = Field; LDateTime a = d; if (AdjustTz) a.ToUtc(); SYSTEMTIME st; st.wDay = a.Day(); st.wMonth = a.Month(); st.wYear = a.Year(); st.wMinute = a.Minutes(); st.wHour = a.Hours(); st.wSecond = a.Seconds(); st.wMilliseconds = 0; if (SystemTimeToFileTime(&st, &Prop.Value.ft)) { HRESULT res = Props->SetProps(1, &Prop, 0); return SUCCEEDED(res); } } return false; } }; class LMapiAddr : public LDataPropI { LMapiStore *Store; public: LMapiMail *m; int CC, Status; LString Name, Email; LMapiAddr(LMapiStore *store); Store3CopyDecl; const char *GetStr(int id); Store3Status SetStr(int id, const char *str); int64 GetInt(int id); Store3Status SetInt(int id, int64 i); }; class LMapiThing : public LDataI, public LMapiBase { friend class LMapiStore; protected: LArray Entry; LPMESSAGE MapiMsg; LString Class; LMapiFolder *Parent; bool IsDirty; public: LMapiStore *Store; LMapiThing(LMapiStore *store); ~LMapiThing(); virtual void Set(SPropValue *entry, LMapiFolder *parent, ScribeMapiList *lst) {} virtual LPMESSAGE Handle() { return MapiMsg; } virtual void ReleaseHandle(); void SetDirty(); uint32_t Type() { LAssert(0); return MAGIC_NONE; } bool IsOnDisk() { LAssert(0); return false; } bool IsOrphan() { LAssert(0); return false; } uint64 Size() { LAssert(0); return 0; } Store3Status Save(LDataI *Parent = 0) { LAssert(0); return Store3Error; } Store3Status Delete(bool ToTrash = true) { LAssert(0); return Store3Error; } LDataStoreI *GetStore(); LAutoStreamI GetStream(const char *file, int line) { LAssert(0); LAutoStreamI s; return s; } Store3Status SetRfc822(LStreamI *m) { LAssert(0); return Store3Error; } }; class LMapiAttachment : public Store3Attachment, public LMapiBase { friend class AttachStream; LPATTACH MapiAttach; LONG AttachNum; ULONG AttachMethod; uint64 DataSize; LString Name; LString MimeType; LString ContentId; LString Charset; LString Literal; public: LMapiAttachment(LMapiStore *store); ~LMapiAttachment(); LPATTACH Handle(); bool Set(LMapiMail *mail, ScribeMapiList *Lst); bool Set(const char *Content, const char *Charset, const char *MimeType); Store3CopyDecl; const char *GetStr(int id); Store3Status SetStr(int id, const char *str); int64 GetInt(int id); Store3Status SetInt(int id, int64 i); uint32_t Type(); bool IsOnDisk(); bool IsOrphan(); uint64 Size(); Store3Status Save(LDataI *Parent = NULL); Store3Status Delete(bool ToTrash = false); LAutoStreamI GetStream(const char *file, int line); void OnSave(); }; class LMapiMail : public LMapiThing { LString Subject; LMapiAddr From; LMapiAddr Reply; DIterator To; LDateTime Date; uint64 Flags; uint64 MsgSize; LString Charset; LString TxtBody; LString HtmlBody; LString MimeType; public: LMapiAttachment *Seg; LMapiMail(LMapiStore *store); ~LMapiMail(); void Set(SPropValue *entry, LMapiFolder *parent, ScribeMapiList *lst); LPMESSAGE Handle(); // LDataPropI API Store3CopyDecl; const char *GetStr(int id); Store3Status SetStr(int id, const char *str); int64 GetInt(int id); Store3Status SetInt(int id, int64 i); const LDateTime *GetDate(int id); Store3Status SetDate(int id, const LDateTime *i); LDataPropI *GetObj(int id); Store3Status SetObj(int id, LDataPropI *i); - GDataIt GetList(int id); + LDataIt GetList(int id); Store3Status SetRfc822(LStreamI *m); // LDataI API uint32_t Type(); bool IsOnDisk(); bool IsOrphan(); uint64 Size(); Store3Status Save(LDataI *Parent); Store3Status Delete(bool ToTrash = true); LAutoStreamI GetStream(const char *file, int line); }; class LMapiCalendar : public LMapiThing { LDateTime StartDt, EndDt; LString Subject, Location, Notes; CalendarShowTimeAs ShowAs; CalendarPrivacyType Priv; LString TimeZone; uint64 Colour; bool Recur; public: LMapiCalendar(LMapiStore *store); ~LMapiCalendar(); void Set(SPropValue *entry, LMapiFolder *parent, ScribeMapiList *lst); LPMESSAGE Handle(); // LDataPropI API LDataPropI &operator =(LDataPropI &p); const char *GetStr(int id); Store3Status SetStr(int id, const char *str); int64 GetInt(int id); Store3Status SetInt(int id, int64 i); LDateTime *GetDate(int id); Store3Status SetDate(int id, const LDateTime *i); LDataPropI *GetObj(int id); - GDataIt GetList(int id); + LDataIt GetList(int id); // LDataI API LDataI &operator =(LDataI &p); uint32_t Type(); bool IsOnDisk(); bool IsOrphan(); uint64 Size(); Store3Status Save(LDataI *Parent); Store3Status Delete(bool ToTrash = true); }; class LMapiContact : public LMapiThing { struct Address { LString Street, Suburb, Postcode, State, Country, Url; }; struct Numbers { LString Number, Fax, Mobile; }; LString PrimaryEmail; LString AltEmails; LString Title, First, Last, Nick, Spouse; Address Home, Work; Numbers HomePh, WorkPh; LString Company, Position; LString Notes; void ReadEmails(); char *CacheGetStr(LString &s, uint32_t Prop); public: LMapiContact(LMapiStore *store); ~LMapiContact(); void Set(SPropValue *entry, LMapiFolder *parent, ScribeMapiList *lst); LPMESSAGE Handle(); // LDataPropI API LDataPropI &operator =(LDataPropI &p); const char *GetStr(int id); Store3Status SetStr(int id, const char *str); int64 GetInt(int id); Store3Status SetInt(int id, int64 i); const LDateTime *GetDate(int id); Store3Status SetDate(int id, const LDateTime *i); LDataPropI *GetObj(int id); - GDataIt GetList(int id); + LDataIt GetList(int id); // LDataI API LDataI &operator =(LDataI &p); uint32_t Type(); bool IsOnDisk(); bool IsOrphan(); uint64 Size(); Store3Status Save(LDataI *Parent); Store3Status Delete(bool ToTrash = true); }; class LMapiFolderField : public LDataPropI { LMapiStore *Store; LString Name; int Id; int Width; public: LMapiFolderField(LMapiStore *store); ~LMapiFolderField(); // LDataPropI API LDataPropI &operator =(LDataPropI &p); const char *GetStr(int id); Store3Status SetStr(int id, const char *str); int64 GetInt(int id); Store3Status SetInt(int id, int64 i); const LDateTime *GetDate(int id); Store3Status SetDate(int id, const LDateTime *i); LDataPropI *GetObj(int id); - GDataIt GetList(int id); + LDataIt GetList(int id); Store3Status SetRfc822(LStreamI *m); }; class LMapiFolder : public LDataFolderI, public LMapiBase { friend class LMapiStore; LPMAPIFOLDER MapiFolder; LArray Entry; LMapiStore *Store; LMapiFolder *Parent; LString Name; LString Class; int64 Unread; bool IsOpen; int SortIndex; uint32_t ItemType; Store3SystemFolder FolderType; DIterator Sub; DIterator Items; DIterator Flds; public: LMapiFolder(LMapiStore *store); ~LMapiFolder(); bool Set(LPMAPIFOLDER f); bool Set(LMapiFolder *parent, ScribeMapiList *Lst); LPMAPIFOLDER Handle(); void ReleaseHandle(); // LDataPropI API Store3CopyDecl; const char *GetStr(int id); Store3Status SetStr(int id, const char *str); int64 GetInt(int id); Store3Status SetInt(int id, int64 i); const LDateTime *GetDate(int id); Store3Status SetDate(int id, const LDateTime *i); LDataPropI *GetObj(int id); - GDataIt GetList(int id); + LDataIt GetList(int id); Store3Status SetRfc822(LStreamI *m); // LDataI API uint32_t Type(); bool IsOnDisk(); bool IsOrphan(); uint64 Size(); Store3Status Save(LDataI *Parent); Store3Status Delete(bool ToTrash = true); LDataStoreI *GetStore(); LAutoStreamI GetStream(const char *file, int line); // LDataFolderI API LDataIterator &SubFolders(); LDataIterator &Children(); LDataIterator &Fields(); Store3Status DeleteAllChildren(); Store3Status FreeChildren(); void OnSelect(bool s); void OnCommand(const char *Name); }; class ScribeMapiList : public LMapiBase { LPMAPITABLE List; ULONG Rows; SRowSet *BaseRow; uint32_t i, StartIndex; bool ReleaseList; bool Status; public: ScribeMapiList(LPMAPITABLE list, bool release = true) { List = list; Rows = 0; BaseRow = 0; i = 0; StartIndex = 0; ReleaseList = release; Status = false; if (List) { HRESULT res = List->GetRowCount(0, &Rows); if (SUCCEEDED(res) && Rows) { res = List->SeekRow(BOOKMARK_BEGINNING, 0, NULL); if (SUCCEEDED(res)) { res = List->QueryRows(Rows, 0, &BaseRow); Status = SUCCEEDED(res); } } } } ~ScribeMapiList() { if (List && ReleaseList) { List->Release(); } } int Index() { return i; } int Length() { return Rows; } bool More() { return (BaseRow && i >= StartIndex && i < BaseRow->cRows + StartIndex); } void Next() { i++; if (BaseRow->cRows < Rows && i-StartIndex >= BaseRow->cRows) { // run into the end of the list section int s = StartIndex + BaseRow->cRows; HRESULT res = List->QueryRows(Rows-StartIndex, 0, &BaseRow); if (SUCCEEDED(res)) { StartIndex = s; } } } SRowSet *Current() { if (More()) // in range { return BaseRow + i - StartIndex; } return 0; } SPropValue *GetField(int Field) { if (More()) // in range { return MapiGetField(BaseRow->aRow + i - StartIndex, Field); } return 0; } }; class MapiEntryRef : public LMapiBase { LMapiStore *Store; public: LString DisplayName; LArray Entry; MapiEntryRef(LMapiStore *store) { Store = store; } bool OpenRoot(LPMAPISESSION Session, UI_TYPE UiHnd, IMsgStore **MsgStore, IMAPIFolder **RootFolder); }; class ScribeMsgStores : public LArray, public LMapiBase { LMapiStore *Store; public: bool Status; ScribeMsgStores(LMapiStore *store, LPMAPISESSION Session) { Store = store; Status = false; Session->AddRef(); ULONG r = Session->Release(); LPMAPITABLE MsgStores = 0; HRESULT res = Session->GetMsgStoresTable(0, &MsgStores); if (SUCCEEDED(res) && MsgStores) { for (ScribeMapiList Lst(MsgStores, false); Lst.More(); Lst.Next()) { SPropValue *DisplayName = Lst.GetField(PR_DISPLAY_NAME); SPropValue *Entry = Lst.GetField(PR_ENTRYID); if (DisplayName && Entry) { LAutoPtr Ref(new MapiEntryRef(Store)); if (Ref) { Ref->DisplayName = MapiCastString(DisplayName); void *p = NULL; int s = 0; if (Ref->DisplayName && MapiCastBinary(Entry, p, s)) { Ref->Entry.Add((uint8_t*)p, s); Add(Ref.Release()); } } } } Status = true; } } ~ScribeMsgStores() { DeleteObjects(); } }; class LMapiStore : public LDataStoreI, public LLibrary { friend class LMapiFolder; friend class LMapiThing; friend class LMapiMail; LDataEventsI *Callback; LMapiFolder *Root; LAutoPtr EntryRef; LString Profile, Username, Password, RootName; uint64 AccountId; LArray InboxEntry; LArray Dirty; LMapiAdviseSink *Notify; bool MapiInitialized; LPMAPISESSION Session; IMsgStore *MsgStore; UI_TYPE Ui; MAPIINITIALIZE *MAPIInitialize; MAPILOGONEX *MAPILogonEx; MAPIUNINITIALIZE *MAPIUninitialize; MAPIALLOCATEBUFFER *MAPIAllocateBuffer; MAPIFREEBUFFER *MAPIFreeBuffer; pWrapCompressedRTFStream WrapCompressedRTFStream; LMapiFolder *FindSystemFolder(Store3SystemFolder Type); public: LMapiStore( const char *Server, const char *Username, const char *Password, uint64 accountId, LDataEventsI *callback); ~LMapiStore(); // Util IMsgStore *Handle() { return MsgStore; } bool Error(const char *Fmt, ...); ULONG OnNotify(ULONG cNotif, LPNOTIFICATION lpNotifications); // MAPI API bool Login(); // LDataPropI API Store3Status SetInt(int id, int64 i); int64 GetInt(int id); const char *GetStr(int id); // LDataEventsI wrappers bool OnChange(const char *File, int Line, LArray &items, int FieldHint); // LDataStoreI API uint64 Size() { return 0; } LDataI *Create(int Type); LDataFolderI *GetRoot(bool create = false); Store3Status Move(LDataFolderI *NewFolder, LArray &Items); Store3Status Delete(LArray &Items, bool ToTrash); Store3Status Change(LArray &Items, int PropId, LVariant &Value, LOperator Operator); void Compact(LViewI *Parent, LDataPropI *Props, std::function OnStatus); void OnEvent(void *Param); bool OnIdle(); LDataEventsI *GetEvents(); }; #endif #endif \ No newline at end of file diff --git a/Code/Store3Mapi/ScribeMapi_Calendar.cpp b/Code/Store3Mapi/ScribeMapi_Calendar.cpp --- a/Code/Store3Mapi/ScribeMapi_Calendar.cpp +++ b/Code/Store3Mapi/ScribeMapi_Calendar.cpp @@ -1,276 +1,276 @@ #include "ScribeMapi.h" #include "Scribe.h" #include "Calendar.h" LMapiCalendar::LMapiCalendar(LMapiStore *store) : LMapiThing(store) { ShowAs = CalBusy; Priv = CalDefaultPriv; Colour = -1; Recur = false; } LMapiCalendar::~LMapiCalendar() { } void LMapiCalendar::Set(SPropValue *entry, LMapiFolder *parent, ScribeMapiList *Lst) { if (Entry.Length(entry->Value.bin.cb)) memcpy(&Entry[0], entry->Value.bin.lpb, entry->Value.bin.cb); Parent = parent; } LPMESSAGE LMapiCalendar::Handle() { if (!MapiMsg && Parent && Parent->Handle()) { ULONG Type = 0; IUnknown *Item = NULL; HRESULT e = Parent->Handle()->OpenEntry( (ULONG)Entry.Length(), (LPENTRYID)&Entry[0], NULL, MAPI_BEST_ACCESS, &Type, &Item); if (SUCCEEDED(e) && Item) { switch (Type) { case MAPI_MESSAGE: { Item->QueryInterface(IID_IMessage, (void**)&MapiMsg); break; } default: { LAssert(0); break; } } if (Item) Item->Release(); } } return MapiMsg; } LDataPropI &LMapiCalendar::operator =(LDataPropI &p) { SetInt(FIELD_CAL_TYPE, p.GetInt(FIELD_CAL_TYPE)); SetInt(FIELD_CAL_COMPLETED, p.GetInt(FIELD_CAL_COMPLETED)); SetDate(FIELD_CAL_START_UTC, p.GetDate(FIELD_CAL_START_UTC)); SetDate(FIELD_CAL_END_UTC, p.GetDate(FIELD_CAL_END_UTC)); SetStr(FIELD_CAL_TIMEZONE, p.GetStr(FIELD_CAL_TIMEZONE)); SetStr(FIELD_CAL_SUBJECT, p.GetStr(FIELD_CAL_SUBJECT)); SetStr(FIELD_CAL_LOCATION, p.GetStr(FIELD_CAL_LOCATION)); SetStr(FIELD_UID, p.GetStr(FIELD_UID)); SetStr(FIELD_CAL_REMINDERS, p.GetStr(FIELD_CAL_REMINDERS)); SetInt(FIELD_CAL_SHOW_TIME_AS, p.GetInt(FIELD_CAL_SHOW_TIME_AS)); SetInt(FIELD_CAL_RECUR, p.GetInt(FIELD_CAL_RECUR)); SetInt(FIELD_CAL_RECUR_FREQ, p.GetInt(FIELD_CAL_RECUR_FREQ)); SetInt(FIELD_CAL_RECUR_INTERVAL, p.GetInt(FIELD_CAL_RECUR_INTERVAL)); SetDate(FIELD_CAL_RECUR_END_DATE, p.GetDate(FIELD_CAL_RECUR_END_DATE)); SetInt(FIELD_CAL_RECUR_END_COUNT, p.GetInt(FIELD_CAL_RECUR_END_COUNT)); SetInt(FIELD_CAL_RECUR_END_TYPE, p.GetInt(FIELD_CAL_RECUR_END_TYPE)); SetStr(FIELD_CAL_RECUR_FILTER_POS, p.GetStr(FIELD_CAL_RECUR_FILTER_POS)); SetInt(FIELD_CAL_RECUR_FILTER_DAYS, p.GetInt(FIELD_CAL_RECUR_FILTER_DAYS)); SetInt(FIELD_CAL_RECUR_FILTER_MONTHS, p.GetInt(FIELD_CAL_RECUR_FILTER_MONTHS)); SetStr(FIELD_CAL_RECUR_FILTER_YEARS, p.GetStr(FIELD_CAL_RECUR_FILTER_YEARS)); SetStr(FIELD_CAL_NOTES, p.GetStr(FIELD_CAL_NOTES)); return *this; } const char *LMapiCalendar::GetStr(int id) { switch (id) { case FIELD_CAL_SUBJECT: if (!Subject) Subject = LFromNativeCp(MapiGetPropStr(Handle(), PR_SUBJECT)); return Subject; case FIELD_CAL_LOCATION: if (!Location) Location = LFromNativeCp(MapiGetPropStr(Handle(), PR_LOCATION)); return Location; case FIELD_CAL_NOTES: if (!Notes) Notes = LFromNativeCp(MapiGetPropStr(Handle(), PR_BODY)); return Notes; case FIELD_CAL_TIMEZONE: return TimeZone; default: LAssert(0); break; } return NULL; } Store3Status LMapiCalendar::SetStr(int id, const char *str) { switch (id) { case FIELD_CAL_SUBJECT: Subject = str; if (MapiSetPropStr(Handle(), PR_SUBJECT, Subject)) return Store3Success; break; case FIELD_CAL_LOCATION: Location = str; if (MapiSetPropStr(Handle(), PR_LOCATION, Location)) return Store3Success; break; case FIELD_CAL_NOTES: Notes = str; if (MapiSetPropStr(Handle(), PR_BODY, Notes)) return Store3Success; break; case FIELD_CAL_TIMEZONE: TimeZone = str; return Store3Success; default: LAssert(0); break; } return Store3Error; } int64 LMapiCalendar::GetInt(int id) { switch (id) { case FIELD_CAL_TYPE: return CalEvent; case FIELD_CAL_RECUR: return Recur; case FIELD_COLOUR: return Colour; case FIELD_CAL_SHOW_TIME_AS: return ShowAs; case FIELD_CAL_PRIVACY: return Priv; default: LAssert(0); break; } return NULL; } Store3Status LMapiCalendar::SetInt(int id, int64 i) { switch (id) { case FIELD_CAL_RECUR: Recur = i != 0; return Store3Success; case FIELD_COLOUR: Colour = i; return Store3Success; case FIELD_CAL_SHOW_TIME_AS: ShowAs = (CalendarShowTimeAs)i; return Store3Success; case FIELD_CAL_PRIVACY: Priv = (CalendarPrivacyType)i; return Store3Success; default: LAssert(0); break; } return Store3Error; } LDateTime *LMapiCalendar::GetDate(int id) { switch (id) { case FIELD_CAL_START_UTC: MapiGetPropDate(StartDt, Handle(), PR_START_DATE); return &StartDt; case FIELD_CAL_END_UTC: MapiGetPropDate(EndDt, Handle(), PR_END_DATE); return &EndDt; default: LAssert(0); break; } return NULL; } Store3Status LMapiCalendar::SetDate(int id, const LDateTime *i) { switch (id) { case FIELD_CAL_START_UTC: if (MapiSetPropDate(Handle(), PR_START_DATE, StartDt)) return Store3Success; break; case FIELD_CAL_END_UTC: if (MapiSetPropDate(Handle(), PR_END_DATE, EndDt)) return Store3Success; break; default: LAssert(0); break; } return Store3Error; } LDataPropI *LMapiCalendar::GetObj(int id) { LAssert(0); return NULL; } -GDataIt LMapiCalendar::GetList(int id) +LDataIt LMapiCalendar::GetList(int id) { LAssert(0); return NULL; } LDataI &LMapiCalendar::operator =(LDataI &p) { return *this; } uint32_t LMapiCalendar::Type() { return MAGIC_CALENDAR; } bool LMapiCalendar::IsOnDisk() { return true; } bool LMapiCalendar::IsOrphan() { return false; } uint64 LMapiCalendar::Size() { return 0; } Store3Status LMapiCalendar::Save(LDataI *Parent) { return Store3Error; } Store3Status LMapiCalendar::Delete(bool ToTrash) { return Store3Error; } diff --git a/Code/Store3Mapi/ScribeMapi_Contact.cpp b/Code/Store3Mapi/ScribeMapi_Contact.cpp --- a/Code/Store3Mapi/ScribeMapi_Contact.cpp +++ b/Code/Store3Mapi/ScribeMapi_Contact.cpp @@ -1,274 +1,274 @@ #include "ScribeMapi.h" #include "Scribe.h" uint32_t MapiContactEmailTags[] = { PR_LAST_MODIFIER_NAME, // 0x3FFA001F 0x800A001F, // PR_EMS_AB_AUTOREPLY_MESSAGE 0x8035001F, // PR_EMS_AB_EXTENSION_ATTRIBUTE_9 0x8036001F, 0x8057001F, 0x80C7001F, 0x80Ca001F, 0x80Cd001F, 0x805d001e, 0x8060001e, 0x81ae001e, 0x81b0001e, 0 }; LMapiContact::LMapiContact(LMapiStore *store) : LMapiThing(store) { } LMapiContact::~LMapiContact() { } void LMapiContact::Set(SPropValue *entry, LMapiFolder *parent, ScribeMapiList *lst) { if (Entry.Length(entry->Value.bin.cb)) memcpy(&Entry[0], entry->Value.bin.lpb, entry->Value.bin.cb); Parent = parent; } LPMESSAGE LMapiContact::Handle() { if (!MapiMsg && Parent && Parent->Handle()) { ULONG Type = 0; IUnknown *Item = NULL; HRESULT e = Parent->Handle()->OpenEntry( (ULONG)Entry.Length(), (LPENTRYID)&Entry[0], NULL, MAPI_BEST_ACCESS, &Type, &Item); if (SUCCEEDED(e) && Item) { switch (Type) { case MAPI_MESSAGE: { Item->QueryInterface(IID_IMessage, (void**)&MapiMsg); break; } default: { LAssert(0); break; } } if (Item) Item->Release(); } } return MapiMsg; } LDataPropI &LMapiContact::operator =(LDataPropI &p) { LAssert(0); return *this; } void LMapiContact::ReadEmails() { if (!PrimaryEmail) { LString::Array Addrs; for (unsigned i=0; MapiContactEmailTags[i]; i++) { LString s = MapiGetPropStr(Handle(), MapiContactEmailTags[i]); if (s && strchr(s, '@')) { bool Has = false; for (unsigned n=0; nGetStr(FIELD_FOLDER_NAME); } LMapiFolder::~LMapiFolder() { if (Store->Root == this) Store->Root = NULL; ReleaseHandle(); } bool LMapiFolder::Set(LPMAPIFOLDER f) { MapiFolder = f; return MapiFolder != NULL; } bool LMapiFolder::Set(LMapiFolder *parent, ScribeMapiList *Lst) { SPropValue *p = Lst->GetField(PR_ENTRYID); if (!p) return false; Entry.Add(p->Value.bin.lpb, p->Value.bin.cb); Parent = parent; if (Lst) { Name = MapiCastString(Lst->GetField(PR_DISPLAY_NAME)); if (Name) { if (!_stricmp(Name, "Deleted Items") || !_stricmp(Name, "Trash")) { FolderType = Store3SystemTrash; ItemType = MAGIC_ANY; } else if (!_stricmp(Name, "Sent")) { FolderType = Store3SystemSent; } else if (!_stricmp(Name, "Outbox")) { FolderType = Store3SystemOutbox; } } Class = MapiCastString(Lst->GetField(PR_CONTAINER_CLASS)); if (Class) { if (!_stricmp(Class, "IPF.Appointment")) ItemType = MAGIC_CALENDAR; else if (!_stricmp(Class, "IPF.Contact")) ItemType = MAGIC_CONTACT; } Unread = MapiCastInt(Lst->GetField(PR_CONTENT_UNREAD)); } if (Parent->Entry.Length() == 0 && Store->InboxEntry.Length() > 0 && Entry.Length() > 0) { // Check if we are the Inbox ULONG Result = 0; HRESULT res = Store->Handle()->CompareEntryIDs( (ULONG)Store->InboxEntry.Length(), (LPENTRYID)&Store->InboxEntry[0], (ULONG)Entry.Length(), (LPENTRYID)&Entry[0], 0, &Result); if (SUCCEEDED(res) && Result) FolderType = Store3SystemInbox; } return true; } LPMAPIFOLDER LMapiFolder::Handle() { if (!MapiFolder) { if (Parent && Parent->MapiFolder) { ULONG Type; HRESULT res = Parent->MapiFolder->OpenEntry ( (ULONG)Entry.Length(), (LPENTRYID)&Entry[0], NULL, MAPI_BEST_ACCESS, &Type, (IUnknown**)&MapiFolder ); if (FAILED(res) || !MapiFolder) { Store->Error("%s:%i - OpenEntry failed with 0x%x\n", _FL, res); } } else LAssert(!"No parent MAPI folders."); } return MapiFolder; } void LMapiFolder::ReleaseHandle() { if (MapiFolder) { MapiFolder->Release(); MapiFolder = NULL; } for (unsigned i=0; iReleaseHandle(); } for (unsigned i=0; iReleaseHandle(); } } Store3CopyImpl(LMapiFolder) { LAssert(0); return false; } const char *LMapiFolder::GetStr(int id) { switch (id) { case FIELD_FOLDER_NAME: return Name; default: LAssert(0); break; } return NULL; } Store3Status LMapiFolder::SetStr(int id, const char *str) { switch (id) { case FIELD_FOLDER_NAME: Name = str; break; default: LAssert(0); return Store3Error; } return Store3Success; } int64 LMapiFolder::GetInt(int id) { switch (id) { case FIELD_FOLDER_TYPE: return ItemType; case FIELD_IS_ONLINE: return true; case FIELD_UNREAD: return Unread; case FIELD_FOLDER_OPEN: return IsOpen; case FIELD_FOLDER_PERM_READ: case FIELD_FOLDER_PERM_WRITE: return PermRequireUser; case FIELD_FOLDER_THREAD: return false; case FIELD_SORT: return SortIndex; case FIELD_FOLDER_INDEX: return -1; case FIELD_LOADED: return Items.State == Store3Loaded; case FIELD_SYSTEM_FOLDER: return FolderType; case FIELD_STORE_TYPE: return Store3Mapi; default: LAssert(0); break; } return -1; } Store3Status LMapiFolder::SetInt(int id, int64 i) { switch (id) { case FIELD_FOLDER_OPEN: { IsOpen = i != 0; break; } case FIELD_SORT: { SortIndex = (int)i; break; } case FIELD_UNREAD: { Unread = i; break; } default: { LAssert(0); return Store3Error; } } return Store3Success; } const LDateTime *LMapiFolder::GetDate(int id) { LAssert(0); return NULL; } Store3Status LMapiFolder::SetDate(int id, const LDateTime *i) { LAssert(0); return Store3Success; } LDataPropI *LMapiFolder::GetObj(int id) { LAssert(0); return NULL; } -GDataIt LMapiFolder::GetList(int id) +LDataIt LMapiFolder::GetList(int id) { LAssert(0); return NULL; } Store3Status LMapiFolder::SetRfc822(LStreamI *m) { LAssert(0); return Store3Error; } uint32_t LMapiFolder::Type() { return MAGIC_FOLDER; } bool LMapiFolder::IsOnDisk() { return true; } bool LMapiFolder::IsOrphan() { return false; } uint64 LMapiFolder::Size() { return 0; } Store3Status LMapiFolder::Save(LDataI *Parent) { return Store3Success; } Store3Status LMapiFolder::Delete(bool ToTrash) { return Store3Error; } LDataStoreI *LMapiFolder::GetStore() { return Store; } LAutoStreamI LMapiFolder::GetStream(const char *file, int line) { LAutoStreamI s; return s; } LDataIterator &LMapiFolder::SubFolders() { #if 1 if (Sub.State == Store3Unloaded && Handle()) { LPMAPITABLE Folders = 0; // Released by ScribeMapiList HRESULT res = MapiFolder->GetHierarchyTable(MAPI_UNICODE , &Folders); if (SUCCEEDED(res)) { for (ScribeMapiList Lst(Folders); Lst.More(); Lst.Next()) { LMapiFolder *SubFolder = new LMapiFolder(Store); if (SubFolder) { SubFolder->Set(this, &Lst); Sub.Insert(SubFolder, -1, true); } } } else Store->Error("%s:%i - GetHierarchyTable failed with %x\n", _FL, res); Sub.State = Store3Loaded; } #endif return Sub; } LDataIterator &LMapiFolder::Children() { if (Items.State == Store3Unloaded && Handle()) { LPMAPITABLE Tbl = 0; HRESULT res = MapiFolder->GetContentsTable(0, &Tbl); if (SUCCEEDED(res)) { for (ScribeMapiList Lst(Tbl); Lst.More(); Lst.Next()) { LAutoPtr t(Store->Create(ItemType)); if (t) { LMapiThing *tptr = dynamic_cast(t.Get()); if (tptr) { tptr->Set(Lst.GetField(PR_ENTRYID), this, &Lst); Items.Insert(tptr, -1, true); t.Release(); } } } } else Store->Error("%s:%i - GetContentsTable failed with %x\n", _FL, res); Items.State = Store3Loaded; } return Items; } LDataIterator &LMapiFolder::Fields() { return Flds; } Store3Status LMapiFolder::DeleteAllChildren() { return Store3Error; } Store3Status LMapiFolder::FreeChildren() { return Store3Error; } void LMapiFolder::OnSelect(bool s) { } void LMapiFolder::OnCommand(const char *Name) { } ////////////////////////////////////////////////////////////////////////// LMapiFolderField::LMapiFolderField(LMapiStore *store) { Store = store; Id = -1; Width = 100; } LMapiFolderField::~LMapiFolderField() { } LDataPropI &LMapiFolderField::operator =(LDataPropI &p) { return *this; } const char *LMapiFolderField::GetStr(int id) { switch (id) { case FIELD_NAME: return Name; default: LAssert(0); break; } return NULL; } Store3Status LMapiFolderField::SetStr(int id, const char *str) { switch (id) { case FIELD_NAME: Name = str; break; default: LAssert(0); return Store3Error; } return Store3Success; } int64 LMapiFolderField::GetInt(int id) { switch (id) { case FIELD_ID: return Id; case FIELD_WIDTH: return Width; default: LAssert(0); break; } return -1; } Store3Status LMapiFolderField::SetInt(int id, int64 i) { switch (id) { case FIELD_ID: Id = (int)i; break; case FIELD_WIDTH: Width = (int)i; break; default: LAssert(0); return Store3Error; } return Store3Success; } const LDateTime *LMapiFolderField::GetDate(int id) { return NULL; } Store3Status LMapiFolderField::SetDate(int id, const LDateTime *i) { return Store3Error; } LDataPropI *LMapiFolderField::GetObj(int id) { return NULL; } -GDataIt LMapiFolderField::GetList(int id) +LDataIt LMapiFolderField::GetList(int id) { return NULL; } Store3Status LMapiFolderField::SetRfc822(LStreamI *m) { return Store3Error; } diff --git a/Code/Store3Mapi/ScribeMapi_Mail.cpp b/Code/Store3Mapi/ScribeMapi_Mail.cpp --- a/Code/Store3Mapi/ScribeMapi_Mail.cpp +++ b/Code/Store3Mapi/ScribeMapi_Mail.cpp @@ -1,558 +1,558 @@ #include "ScribeMapi.h" #include "lgi/common/Store3MimeTree.h" LMapiMail::LMapiMail(LMapiStore *store) : LMapiThing(store), From(store), Reply(store) { Subject = NULL; Flags = 0; MsgSize = 0; Seg = NULL; TxtBody = NULL; HtmlBody = NULL; From.m = this; Reply.m = this; } LMapiMail::~LMapiMail() { To.DeleteObjects(); DeleteObj(Seg); } void LMapiMail::Set(SPropValue *entry, LMapiFolder *parent, ScribeMapiList *Lst) { Entry.Add((uint8_t*)entry->Value.bin.lpb, entry->Value.bin.cb); Parent = parent; if (Lst) { SPropValue *p = Lst->GetField(PR_CLIENT_SUBMIT_TIME); if (p) MapiCastDate(Date, p); p = Lst->GetField(PR_SUBJECT); if (p) Subject = MapiCastString(p); From.Name = MapiCastString(Lst->GetField(PR_SENT_REPRESENTING_NAME)); if (!From.Name) From.Name = MapiCastString(Lst->GetField(PR_SENDER_NAME)); From.Email = MapiCastString(Lst->GetField(PR_SENDER_EMAIL_ADDRESS)); Class = MapiCastString(Lst->GetField(PR_ORIG_MESSAGE_CLASS)); if (!Class) Class = MapiCastString(Lst->GetField(PR_MESSAGE_CLASS)); bool Post = Class ? _strnicmp(Class, "IPM.Post", 8) == 0 : false; int64 f = MapiCastInt(Lst->GetField(PR_MESSAGE_FLAGS)); if (f & (MSGFLAG_SUBMIT | MSGFLAG_UNSENT) && !Post) Flags |= MAIL_CREATED; else Flags |= MAIL_RECEIVED; if (f & MSGFLAG_HASATTACH) Flags |= MAIL_ATTACHMENTS; if (f & MSGFLAG_READ) Flags |= MAIL_READ; MsgSize = MapiCastInt(Lst->GetField(PR_MESSAGE_SIZE)); TxtBody = MapiCastString(Lst->GetField(PR_BODY)); HtmlBody = MapiCastString(Lst->GetField(PR_BODY_HTML)); } } static int HandleLoads = 0; static uint64 HandleTs = 0; LPMESSAGE LMapiMail::Handle() { if (!MapiMsg && Parent && Parent->Handle()) { ULONG Type = 0; IUnknown *Item = NULL; HRESULT e = Parent->Handle()->OpenEntry( (ULONG)Entry.Length(), (LPENTRYID)&Entry[0], NULL, MAPI_BEST_ACCESS, &Type, &Item); if (SUCCEEDED(e) && Item) { HandleLoads++; switch (Type) { case MAPI_MESSAGE: { Item->QueryInterface(IID_IMessage, (void**)&MapiMsg); break; } default: { LAssert(0); break; } } if (Item) Item->Release(); if (LCurrentTime() - HandleTs > 500) { HandleTs = LCurrentTime(); LgiTrace("HandleLoads = %i\n", HandleLoads); } } else Store->Error("%s:%i - OpenEntry failed with 0x%x\n", _FL, e); } return MapiMsg; } Store3CopyImpl(LMapiMail) { return false; } const char *LMapiMail::GetStr(int id) { switch (id) { // Mail fields case FIELD_INTERNET_HEADER: return MapiGetPropStr(Handle(), PR_TRANSPORT_MESSAGE_HEADERS); case FIELD_MIME_TYPE: if (!MimeType) { char *Hdrs = MapiGetPropStr(Handle(), PR_TRANSPORT_MESSAGE_HEADERS); LAutoString c(InetGetHeaderField(Hdrs, "Content-Type", -1)); char *semi = c ? strchr(c, ';') : NULL; if (semi) *semi = 0; MimeType = c; MimeType = MimeType.Strip(); } return MimeType; case FIELD_MESSAGE_ID: return MapiGetPropStr(Handle(), PR_INTERNET_MESSAGE_ID); case FIELD_SUBJECT: if (!Subject) Subject = MapiGetPropStr(Handle(), PR_SUBJECT); return Subject; case FIELD_TEXT: if (!TxtBody) TxtBody = MapiGetPropStr(Handle(), PR_BODY); return TxtBody; case FIELD_ALTERNATE_HTML: if (!HtmlBody && Handle()) { IStream *Html = NULL; HRESULT res = Handle()->OpenProperty(PR_HTML, &IID_IStream, 0, 0, (IUnknown**) &Html); if (SUCCEEDED(res)) { STATSTG s; HRESULT res = Html->Stat(&s, STATFLAG_DEFAULT); if (SUCCEEDED(res)) { HtmlBody.Set(NULL, (NativeInt)s.cbSize.QuadPart); ULONG Rd = 0; res = Html->Read(HtmlBody.Get(), (ULONG)HtmlBody.Length(), &Rd); if (FAILED(res)) { HtmlBody.Empty(); } } Html->Release(); } } if (!HtmlBody) HtmlBody = MapiGetPropStr(Handle(), PR_BODY_HTML); return HtmlBody; case FIELD_HTML_CHARSET: case FIELD_CHARSET: if (!Charset) { int Cp = (int)MapiGetPropInt(Handle(), PR_INTERNET_CPID); Charset = LAnsiToLgiCp(Cp); if (!Charset) LgiTrace("%s:%i - No charset for cp %i\n", _FL, Cp); } return Charset; case FIELD_LABEL: return NULL; default: LAssert(0); break; } return NULL; } Store3Status LMapiMail::SetStr(int id, const char *str) { switch (id) { case FIELD_SUBJECT: { if (MapiSetPropStr(Handle(), PR_SUBJECT, str)) SetDirty(); else return Store3Error; break; } default: { LAssert(0); return Store3Error; } } return Store3Success; } int64 LMapiMail::GetInt(int id) { switch (id) { case FIELD_FLAGS: return Flags; case FIELD_SIZE: return MsgSize; case FIELD_DONT_SHOW_PREVIEW: return true; case FIELD_COLOUR: return 0; case FIELD_PRIORITY: return 0; case FIELD_ACCOUNT_ID: return Store->GetInt(FIELD_ACCOUNT_ID); default: LAssert(0); break; } return NULL; } Store3Status LMapiMail::SetInt(int id, int64 i) { switch (id) { case FIELD_FLAGS: { bool ReadChange = ((i & MAIL_READ) != 0) ^ ((Flags & MAIL_READ) != 0); Flags = i; if (ReadChange && Handle()) { HRESULT res = MapiMsg->SetReadFlag(Flags & MAIL_READ ? 0 : CLEAR_READ_FLAG); if (FAILED(res)) Store->Error("%s:%i - SetReadFlag failed with %x\n", _FL, res); } break; } default: { LAssert(0); return Store3Error; } } return Store3Success; } const LDateTime *LMapiMail::GetDate(int id) { switch (id) { case FIELD_DATE_RECEIVED: case FIELD_DATE_SENT: if (Date.Year() == 0) MapiGetPropDate(Date, Handle(), PR_CLIENT_SUBMIT_TIME); return &Date; default: LAssert(0); break; } return NULL; } Store3Status LMapiMail::SetDate(int id, const LDateTime *i) { switch (id) { case FIELD_DATE_RECEIVED: case FIELD_DATE_SENT: if (MapiSetPropDate(Handle(), PR_CLIENT_SUBMIT_TIME, *i)) return Store3Success; break; default: LAssert(0); break; } return Store3Error; } LDataPropI *LMapiMail::GetObj(int id) { switch (id) { case FIELD_FROM: return &From; case FIELD_REPLY: return &Reply; case FIELD_MIME_SEG: if (!Seg && Handle()) { LPMAPITABLE hAttach = NULL; HRESULT res = Handle()->GetAttachmentTable(MAPI_UNICODE, &hAttach); if (SUCCEEDED(res)) { LArray Segs; for (ScribeMapiList Lst(hAttach); Lst.More(); Lst.Next()) { LAutoPtr a(new LMapiAttachment(Store)); if (a->Set(this, &Lst)) Segs.Add(a.Release()); } auto Txt = GetStr(FIELD_TEXT); if (Txt) { LAutoPtr a(new LMapiAttachment(Store)); if (a && a->Set(Txt, GetStr(FIELD_CHARSET), "text/plain")) Segs.Add(a.Release()); } auto Html = GetStr(FIELD_ALTERNATE_HTML); if (Html) { LAutoPtr a(new LMapiAttachment(Store)); if (a && a->Set(Html, GetStr(FIELD_HTML_CHARSET), "text/html")) Segs.Add(a.Release()); } Store3MimeTree Tree(this, Seg); for (unsigned i=0; iGetRecipientTable(MAPI_UNICODE, &Recipients)) && Recipients) { for (ScribeMapiList Lst(Recipients); Lst.More(); Lst.Next()) { SPropValue *Name = Lst.GetField(PR_DISPLAY_NAME_W); SPropValue *Email1 = Lst.GetField(PR_EMAIL_ADDRESS); SPropValue *Email2 = Lst.GetField(PR_SMTP_ADDRESS); LAutoPtr a(new LMapiAddr(Store)); if ((Name || Email1 || Email2) && a) { a->Name = MapiCastString(Name); if (strchr(MapiCastString(Email1), '@')) a->Email = MapiCastString(Email1); else a->Email = MapiCastString(Email2); SPropValue *Type = Lst.GetField(PR_RECIPIENT_TYPE); if (Type) { int64 Flags = MapiCastInt(Type); if (Flags & MAPI_TO) a->CC = MAIL_ADDR_TO; else if (Flags & MAPI_CC) a->CC = MAIL_ADDR_CC; else if (Flags & MAPI_BCC) a->CC = MAIL_ADDR_BCC; } a->m = this; To.Insert(a.Release(), -1, true); } } } To.State = Store3Loaded; } return &To; } default: LAssert(0); break; } return NULL; } Store3Status LMapiMail::SetRfc822(LStreamI *m) { // IConverterSession does the handling of converting MIME to MAPI (and back) LAssert(0); return Store3Error; } uint32_t LMapiMail::Type() { return MAGIC_MAIL; } bool LMapiMail::IsOnDisk() { return true; } bool LMapiMail::IsOrphan() { return false; } uint64 LMapiMail::Size() { LAssert(0); return 0; } Store3Status LMapiMail::Save(LDataI *Parent) { LAssert(0); return Store3Error; } Store3Status LMapiMail::Delete(bool ToTrash) { LArray del; del.Add(this); return Store->Delete(del, true); } LAutoStreamI LMapiMail::GetStream(const char *file, int line) { LAutoStreamI s; LAssert(0); return s; } //////////////////////////////////////////// LMapiAddr::LMapiAddr(LMapiStore *store) { Store = store; CC = 0; Status = 0; m = NULL; } Store3CopyImpl(LMapiAddr) { CC = (int)p.GetInt(FIELD_CC); Name = p.GetStr(FIELD_NAME); Email = p.GetStr(FIELD_EMAIL); return true; } const char *LMapiAddr::GetStr(int id) { if (!m) { LAssert(0); return NULL; } switch (id) { case FIELD_NAME: return Name; case FIELD_EMAIL: return Email; default: LAssert(0); break; } return NULL; } Store3Status LMapiAddr::SetStr(int id, const char *str) { if (!m) { LAssert(0); return Store3Error; } switch (id) { case FIELD_NAME: Name = str; return Store3Success; case FIELD_EMAIL: Email = str; return Store3Success; default: LAssert(0); break; } return Store3Error; } int64 LMapiAddr::GetInt(int id) { switch (id) { case FIELD_CC: return CC; case FIELD_STATUS: return Status; } LAssert(0); return -1; } Store3Status LMapiAddr::SetInt(int id, int64 i) { switch (id) { case FIELD_CC: CC = (int)i; break; case FIELD_STATUS: Status = (int)i; break; default: LAssert(0); return Store3Error; } return Store3Success; } diff --git a/Code/Store3Webdav/WebdavStore.h b/Code/Store3Webdav/WebdavStore.h --- a/Code/Store3Webdav/WebdavStore.h +++ b/Code/Store3Webdav/WebdavStore.h @@ -1,339 +1,339 @@ #ifndef _WEBDAV_STORE_H_ #define _WEBDAV_STORE_H_ #include "lgi/common/WebDav.h" ////////////////////////////////////////////////////////////////////// class WebdavFolder; class WebdavCalendar; class WebdavThread; struct WebdavEvent; class WebdavStore : public LDataStoreI { friend class WebdavThread; friend class WebdavCalendar; friend class WebdavContact; public: struct LRemote { LString Name, Url, User, Pass; }; protected: ScribeWnd *App; LRemote Remote; LDataEventsI *Callback; LString ContactUrl, CalUrl; WebdavFolder *Root, *ContactFolder, *CalFolder; LXmlTag *LockSettings(const char *File, int Line); void UnlockSettings(); public: WebdavStore(ScribeWnd *a, LDataEventsI *cb, LString optsPath); ~WebdavStore(); // Actions /* bool Read(); bool Write(); bool Delete(); Calendar *NewEvent(); bool Match(char *Email); bool GetEvents(LDateTime &Start, LDateTime &End, LArray &Events); */ // Events void OnChanged(); // LDataPropI int64 GetInt(int id); const char *GetStr(int id); LDataPropI *GetObj(int id); // LDataStoreI impl uint64 Size() { return 0; } LDataI *Create(int Type); LDataFolderI *GetRoot(bool create = false); Store3Status Move(LDataFolderI *NewFolder, LArray &Items) { return Store3Error; } Store3Status Delete(LArray &Items, bool ToTrash); Store3Status Change(LArray &Items, int PropId, LVariant &Value, LOperator Operator) { return Store3Error; } void Compact(LViewI *Parent, LDataPropI *Props, std::function OnStatus) { if (OnStatus) OnStatus(true); } void OnEvent(void *Param); bool OnIdle() { return true; } LDataEventsI *GetEvents() { return NULL; } // LDataEventsI impl void Post(LDataStoreI *store, void *Param); void OnNew(LDataFolderI *parent, LArray &new_items, int pos, bool is_new); bool OnDelete(LDataFolderI *parent, LArray &items); bool OnMove(LDataFolderI *new_parent, LDataFolderI *old_parent, LArray &items); bool OnChange(LArray &items, int FieldHint); }; ///////////////////////////////////////////////////////////////////////////////////// class WebdavObj : public LDataI { friend class WebdavStore; protected: WebdavStore *Store; LString Href; bool Converted; WebdavFolder *Parent; public: WebdavObj(WebdavStore *store) { Store = store; Parent = NULL; } const char *GetHref() { return Href; } virtual void FireOnChange(int Fld) {} virtual bool ConvertToText() { return false; } // LDataI impl uint32_t Type() { return MAGIC_NONE; } bool IsOnDisk() { return false; } bool IsOrphan() { return Store != NULL; } LDataStoreI *GetStore() { return Store; } // Stubs uint64 Size() { return 0; } Store3Status Save(LDataI *Parent = NULL) { return Store3NotImpl; } Store3Status Delete(bool ToTrash = true) { return Store3NotImpl; } LAutoStreamI GetStream(const char *file, int line) { return LAutoStreamI(NULL); } bool SetStream(LAutoStreamI stream) { return false; } bool ParseHeaders() { return false; } LDataPropI *GetObj(int id) { EmptyVirtual(NULL); } Store3Status SetObj(int id, LDataPropI *i) { EmptyVirtual(Store3Error); } - GDataIt GetList(int id) { EmptyVirtual(NULL); } + LDataIt GetList(int id) { EmptyVirtual(NULL); } Store3Status SetRfc822(LStreamI *Rfc822Msg) { return Store3Error; } }; class WebdavFld : public LDataPropI { LDataStoreI *Store; WebdavFolder *Parent; int Id, Width; public: WebdavFld(LDataStoreI *s, WebdavFolder *p = 0, int id = 0, int width = 100); const char *GetStr(int id); int64 GetInt(int id); Store3Status SetInt(int id, int64 i); }; class WebdavFolder : public LDataFolderI { friend class WebdavStore; WebdavFolder &operator =(const WebdavFolder &f) = delete; WebdavStore *Store; WebdavFolder *Parent; LString Name; Store3ItemTypes ItemType = MAGIC_ANY; int64 Sort = 0; Store3State State = Store3Unloaded; public: LString Url; const char *Extension = NULL; DIterator Items; DIterator Sub; DIterator Field; LAutoPtr Thread; WebdavFolder(WebdavStore *store, WebdavFolder *parent = NULL); LString AllocateAddress(); // LDataPropI impl bool CopyProps(LDataPropI &p); const char *GetStr(int id); Store3Status SetStr(int id, const char *str); int64 GetInt(int id); Store3Status SetInt(int id, int64 i); // LDataI impl uint32_t Type() { return MAGIC_FOLDER; } bool IsOnDisk() { return false; } bool IsOrphan() { return false; } uint64 Size() { return 0; } Store3Status Save(LDataI *Parent = NULL); Store3Status Delete(bool ToTrash = true); LDataStoreI *GetStore() { return Store; } LAutoStreamI GetStream(const char *file, int line); bool SetStream(LAutoStreamI stream) { return false; } bool ParseHeaders() { return false; } // LDataFolderI impl LDataIterator &SubFolders(); LDataIterator &Children(); LDataIterator &Fields(); }; ///////////////////////////////////////////////////////////////////////////////////// #define WebdavCalendarDates() \ _(FIELD_CAL_START_UTC, Start) \ _(FIELD_CAL_END_UTC, End) \ _(FIELD_CAL_RECUR_END_DATE, RecurEnd) \ _(FIELD_CAL_LAST_CHECK, LastCheck) \ _(FIELD_DATE_MODIFIED, DateMod) #define WebdavCalendarInts() \ _(FIELD_CAL_TYPE, ObjType) \ _(FIELD_CAL_COMPLETED, Completed) \ _(FIELD_CAL_SHOW_TIME_AS, ShowTimeAs) \ _(FIELD_CAL_RECUR, Recur) \ _(FIELD_CAL_RECUR_FREQ, RecurFreq) \ _(FIELD_CAL_RECUR_INTERVAL, RecurInterval) \ _(FIELD_CAL_RECUR_END_COUNT, RecurEndCount) \ _(FIELD_CAL_RECUR_END_TYPE, RecurEndType) \ _(FIELD_CAL_RECUR_FILTER_DAYS, RecurFilterDays) \ _(FIELD_CAL_RECUR_FILTER_MONTHS, RecurFilterMonths) \ _(FIELD_CAL_ALL_DAY, AddDay) \ _(FIELD_CAL_PRIVACY, Privacy) \ _(FIELD_COLOUR, Colour) \ _(FIELD_STATUS, StoreStatus) #define WebdavCalendarStrings() \ _(FIELD_UID, Uid) \ _(FIELD_CAL_TIMEZONE, TimeZone) \ _(FIELD_CAL_SUBJECT, Subject) \ _(FIELD_CAL_LOCATION, Location) \ _(FIELD_CAL_REMINDERS, Reminders) \ _(FIELD_CAL_RECUR_FILTER_POS, FilterPos) \ _(FIELD_CAL_RECUR_FILTER_YEARS, FilterYears) \ _(FIELD_CAL_NOTES, Notes) \ _(FIELD_CAL_STATUS, CalStatus) class WebdavCalendar : public WebdavObj { LString vCal, Href; #define _(f,v) LDateTime v; WebdavCalendarDates() #undef _ #define _(f,v) uint64_t v = 0; WebdavCalendarInts() #undef _ #define _(f,v) LString v; WebdavCalendarStrings() #undef _ public: WebdavCalendar(WebdavStore *store, WebdavEvent *e); void FireOnChange(int Fld); bool ConvertToText(); // Stubs uint32_t Type() { return MAGIC_CALENDAR; } uint64 Size() { return vCal.Length(); } // LDataI Impl Store3Status Save(LDataI *Parent = 0); Store3Status Delete(bool ToTrash = true); LAutoStreamI GetStream(const char *file, int line); bool CopyProps(LDataPropI &p); const char *GetStr(int id); Store3Status SetStr(int id, const char *str); int64 GetInt(int id); Store3Status SetInt(int id, int64 i); const LDateTime *GetDate(int id); Store3Status SetDate(int id, const LDateTime *i); }; ///////////////////////////////////////////////////////////////////////////////////// #define WebdavContactDates() \ _(FIELD_DATE_MODIFIED, DateMod) #define WebdavContactVariants() \ _(FIELD_CONTACT_IMAGE, Image) #define WebdavContactInts() \ _(FIELD_STATUS, Status) #define WebdavContactStrings() \ _(FIELD_UID, Uid) \ _(FIELD_TITLE, Title) \ _(FIELD_FIRST_NAME, First) \ _(FIELD_LAST_NAME, Last) \ _(FIELD_EMAIL, Email) \ _(FIELD_ALT_EMAIL, AltEmail) \ _(FIELD_NICK, Nick) \ _(FIELD_SPOUSE, Spouse) \ _(FIELD_NOTE, Notes) \ _(FIELD_TIMEZONE, TimeZone) \ \ _(FIELD_HOME_STREET, HomeStreet) \ _(FIELD_HOME_SUBURB, HomeSuburb) \ _(FIELD_HOME_POSTCODE, HomePostCode) \ _(FIELD_HOME_STATE, HomeState) \ _(FIELD_HOME_COUNTRY, HomeCountry) \ _(FIELD_HOME_PHONE, HomePhone) \ _(FIELD_HOME_MOBILE, HomeMobile) \ _(FIELD_HOME_IM, HomeIM) \ _(FIELD_HOME_FAX, HomeFax) \ _(FIELD_HOME_WEBPAGE, HomeWebpage) \ \ _(FIELD_WORK_STREET, WorkStreet) \ _(FIELD_WORK_SUBURB, WorkSuburb) \ _(FIELD_WORK_POSTCODE, WorkPostCode) \ _(FIELD_WORK_STATE, WorkState) \ _(FIELD_WORK_COUNTRY, WorkCountry) \ _(FIELD_WORK_PHONE, WorkPhone) \ _(FIELD_WORK_MOBILE, WorkMobile) \ _(FIELD_WORK_IM, WorkIM) \ _(FIELD_WORK_FAX, WorkFax) \ _(FIELD_WORK_WEBPAGE, WorkWebpage) \ _(FIELD_COMPANY, Company) \ \ _(FIELD_CONTACT_JSON, JSON) class WebdavContact : public WebdavObj { bool Converted; LString vCard; #define _(f,v) LDateTime v; WebdavContactDates() #undef _ #define _(f,v) LVariant v; WebdavContactVariants() #undef _ #define _(f,v) int64 v = 0; WebdavContactInts() #undef _ #define _(f,v) LString v; WebdavContactStrings() #undef _ public: WebdavContact(WebdavStore *store, WebdavEvent *e); void FireOnChange(int Fld); bool ConvertToText(); // Stubs uint32_t Type() { return MAGIC_CONTACT; } uint64 Size() { return vCard.Length(); } // LDataI Impl Store3Status Save(LDataI *Parent = 0); Store3Status Delete(bool ToTrash = true); LAutoStreamI GetStream(const char *file, int line); bool CopyProps(LDataPropI &p); const char *GetStr(int id); Store3Status SetStr(int id, const char *str); int64 GetInt(int id); Store3Status SetInt(int id, int64 i); const LDateTime *GetDate(int id); Store3Status SetDate(int id, const LDateTime *i); const LVariant *GetVar(int id); Store3Status SetVar(int id, LVariant *i); }; #endif diff --git a/UnitTests/Test_Imap.cpp b/UnitTests/Test_Imap.cpp --- a/UnitTests/Test_Imap.cpp +++ b/UnitTests/Test_Imap.cpp @@ -1,302 +1,302 @@ #include "lgi/common/Lgi.h" #include "lgi/common/Store3.h" #include "UnitTest.h" #include "ScribeDefs.h" class Printf : public LStream { public: ssize_t Write(const void *Buffer, ssize_t Size, int Flags = 0) { if (Flags == 0) printf("%.*s", (int)Size, (char*)Buffer); return Size; } }; struct ImapTestPriv : public LDataEventsI { LViewI *App; LDataStoreI *Imap; Printf Log; bool Status; LDataFolderI *Inbox, *Test; ImapTestPriv(LViewI *app) { App = app; Status = true; Imap = 0; Inbox = Test = 0; } void Shutdown() { LDataStoreI *Temp = Imap; Imap = 0; int64 Start = LCurrentTime(); DeleteObj(Temp); int64 Time = LCurrentTime() - Start; if (Time > 1000) { Status = false; printf("%s:%i - Error: took to long to shutdown IMAP store (" LPrintfInt64 "ms)\n", _FL, Time); } } void Post(LDataStoreI *store, void *Param) { App->PostEvent(M_STORAGE_EVENT, (LMessage::Param)store, (LMessage::Param)Param); } bool GetSystemPath(int Folder, LVariant &Path) { return false; } LOptionsFile *GetOptions(bool Create = false) { return 0; } void OnNew(LDataFolderI *parent, LDataI *new_item, int pos) { } bool OnDelete(LDataFolderI *parent, LDataI *del_item) { return false; } bool OnMove(LDataFolderI *new_parent, LDataFolderI *old_parent, LDataI *item) { return false; } bool OnChange(LDataI *update_item) { return false; } void OnIntPropChange(LDataStoreI *store, int prop) { } void OnStrPropChange(LDataStoreI *store, int prop) { } void OnNew(LDataFolderI *parent, LArray &new_items, int pos, bool is_new) { } bool OnDelete(LDataFolderI *parent, LArray &items) { return true; } bool OnMove(LDataFolderI *new_parent, LDataFolderI *old_parent, LArray &items) { return true; } bool OnChange(LArray &items, int FieldHint) { return true; } }; ImapTest::ImapTest(LViewI *app) : UnitTest(app, "Imap Store3") { d = new ImapTestPriv(app); } ImapTest::~ImapTest() { DeleteObj(d); } bool ImapTest::OnIdle() { return d->Imap ? d->Imap->OnIdle() : false; } void ImapTest::OnEvent(LMessage *m) { switch (m->Msg()) { case M_STORAGE_EVENT: { if (d->Imap) { LDataStoreI *Store = (LDataStoreI*)m->A(); if (Store) { Store->OnEvent((void*)m->B()); } } break; } } } void ImapTest::ClearCache() { // Clear any previous cache char c[MAX_PATH_LEN]; if (LGetSystemPath(LSP_APP_INSTALL, c, sizeof(c))) { LMakePath(c, sizeof(c), c, "ImapCache"); FileDev->RemoveFolder(c, true); } } bool ImapTest::Login() { char *User = 0, *Pass = 0, *Host = 0; if (LDirExists("h:\\Code")) { Host = "localhost"; User = "fret@memecode.dyndns.org"; Pass = "_groon"; } else { Host = "imap"; User = "matthew"; Pass = "_black6"; } // Create Imap store... LAutoPtr store; if (d->Imap = OpenImap(Host, 0, User, Pass, 0, d, NULL, NULL, &d->Log, 1234, store)) { // Wait for it to connect and go online int64 Start = LCurrentTime(); while (!d->Imap->GetInt(FIELD_IS_ONLINE) && (LCurrentTime() - Start) < 5000) { LSleep(100); } int64 Time = LCurrentTime() - Start; if (Time >= 5000) { printf("\t%s:%i - Error: IMAP store took too long to go online (%ims).\n", _FL, (int)Time); DeleteObj(d->Imap); return false; } return true; } return false; } bool ImapTest::Test01_LoadInbox() { // Create imap store... d->Log.Print(" Logging in.\n"); if (Login()) { LDataFolderI *r = d->Imap->GetRoot(); if (r) { if (r->SubFolders().Length() == 0) { printf("%s:%i - Error: IMAP root folder has no sub-folders.\n", _FL); d->Status = false; } else { d->Log.Print("\tGetting sub-folders...\n"); d->Log.Print("\tRoot folder has %i sub-folders\n", r->SubFolders().Length()); for (LDataFolderI *c = r->SubFolders().First(); c; c = r->SubFolders().Next()) { auto Name = c->GetStr(FIELD_FOLDER_NAME); if (!Name) { printf("\t%s:%i - Error: IMAP folder has no name.\n", _FL); d->Status = false; break; } else { printf("\t\t%s\n", Name); if (stricmp(Name, "inbox") == 0) d->Inbox = c; } } if (!d->Inbox) { printf("\t%s:%i - Error: No IMAP inbox.\n", _FL); d->Status = false; } else { int Len = d->Inbox->Children().Length(); printf("Inbox currently has %i email.\n", Len); if (!Len) { int64 Start = LCurrentTime(), Last = 0; while (!d->Inbox->GetInt(FIELD_LOADED)) { if (!Last || LCurrentTime() - Last > 15000) { printf("\tWaiting folder load... %" LPrintfSizeT "\n", d->Inbox->Children().Length()); Last = LCurrentTime(); } LSleep(1000); } printf("\tLoad took %i\n", (int)(LCurrentTime()-Start)); } printf("\tInbox now has %" LPrintfSizeT " email.\n", d->Inbox->Children().Length()); /* int n = 0; for (LDataI *m = d->Inbox->Children().First(); m; m = d->Inbox->Children().Next(), n++) { if (n < 20) { printf("\t[%i] %.90s\n\t\tTo: ", n, m->GetStr(FIELD_SUBJECT)); - GDataIt It = m->GetList(FIELD_TO); + LDataIt It = m->GetList(FIELD_TO); int k = 0; for (LDataPropI *i=It->First(); i && k < 3; i=It->Next(), k++) { if (i->GetStr(FIELD_NAME)) printf("%s <%s>, ", i->GetStr(FIELD_NAME), i->GetStr(FIELD_EMAIL)); else printf("<%s>, ", i->GetStr(FIELD_EMAIL)); } printf("\n\t\tFlags: %s\n", m->GetStr(FIELD_CACHE_FLAGS)); } else if (n == 20) printf("\t.......snip..........\n"); } */ } } } else { printf("%s:%i - Error: Failed to get IMAP root folder.\n", _FL); d->Status = false; } printf("Profile: %.1f listing, %.1f select\n", (double)d->Imap->GetInt(FIELD_PROFILE_IMAP_LISTING)/1000.0, (double)d->Imap->GetInt(FIELD_PROFILE_IMAP_SELECT)/1000.0); // Shut it down d->Shutdown(); } else d->Status = false; return d->Status; } bool ImapTest::Test() { return Test01_LoadInbox(); }