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; + SetObjectName(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(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, 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/Exp_Scribe.cpp b/Code/Exp_Scribe.cpp --- a/Code/Exp_Scribe.cpp +++ b/Code/Exp_Scribe.cpp @@ -1,927 +1,954 @@ #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" /* 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 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 NULL; } 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; + int ObjectCreated = 0; + int ObjectErrors = 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; } 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)); } 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) SetCtrlName(IDC_DEST, dlg->Name()); 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) { if (Stricmp(n->GetText(), s->Get()) == 0) { Has = true; break; } } if (!Has) { 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: { if (!Dst) Dst.LoadFolders(GetCtrlName(IDC_DEST)); if (Dst.Root) { 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: { Params.AllFolders = GetCtrlValue(IDC_ALL) != 0; Params.ExceptTrashSpam = GetCtrlValue(IDC_NO_SPAM_TRASH) != 0; Params.DestPath = GetCtrlName(IDC_FOLDER); Params.DestFolders = GetCtrlName(IDC_DEST); if (Lst) { + Params.SrcPaths.Empty(); + Params.SrcPaths.SetFixedLength(false); for (auto i: *Lst) Params.SrcPaths.Add(i->GetText()); } 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; } }; 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) { 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) { 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(); }); }); } 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++; // 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; } } */ 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; } 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()); 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 { State = ExpGetNext; } }); break; } case ExpLoadFolders: { // No-op, but we should probably time out... break; } case ExpItems: { if (!SrcFolder || !DstFolder || !DstStore) { State = ExpGetNext; break; } 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()) { case MAGIC_MAIL: { 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); 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 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); + auto outObj = DstStore->Create(in->Type()); + if (!outObj) + { + ObjectErrors++; + LgiTrace("%s:%i - %s failed to create %s\n", _FL, + DstStore->GetStr(FIELD_STORE_TYPE), + Store3ItemTypeName((Store3ItemTypes)in->Type())); + break; + } + + if (!outObj->CopyProps(*in)) + { + ObjectErrors++; + break; + } + + if (outObj->Save(DstFolder->GetObject()) < Store3Delayed) + { + ObjectErrors++; + break; + } + + ObjectCreated++; break; } } Processed++; - } if (SrcItems.Length() == 0) { SrcFolder = NULL; DstFolder = NULL; DstStore = NULL; State = ExpGetNext; (*this)++; // move progress... } 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/Scribe.h b/Code/Scribe.h --- a/Code/Scribe.h +++ b/Code/Scribe.h @@ -1,2564 +1,2563 @@ /*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; } 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/ScribeFolderSelect.cpp b/Code/ScribeFolderSelect.cpp --- a/Code/ScribeFolderSelect.cpp +++ b/Code/ScribeFolderSelect.cpp @@ -1,380 +1,380 @@ /* ** FILE: ScribeFolderSelect.cpp ** AUTHOR: Matthew Allen ** DATE: 1/6/1999 ** DESCRIPTION: Scribe Folder Selection ** ** Copyright (C) 1999, Matthew Allen ** fret@memecode.com */ #include #include #include #include #include "Scribe.h" #include "lgi/common/Button.h" #include "resdefs.h" class FolderDlgPriv; class ScribeFolderTree : public LTree { void AddFolder(FolderDlgPriv *d, LTreeItem *i, ScribeFolder *f); public: ScribeFolderTree(int id, int x, int y, int cx, int cy); void Setup(FolderDlgPriv *d, ScribeFolder *Root, const char *InitSel); LString Get2(); }; class FolderDlgPriv { public: FolderDlg *Dlg = NULL; ScribeWnd *App = NULL; bool CreateNew = false; int LimitTo = MAGIC_NONE; LString Path; LString DefaultNewFolderName; LString Filter; ScribeFolderTree *View = NULL; FolderDlgPriv(ScribeWnd *app, FolderDlg *t, bool create) { CreateNew = create; Dlg = t; App = app; } void OnFilter(); }; ////////////////////////////////////////////////////////////////////////////// struct FolderLeaf : public LTreeItem { FolderDlgPriv *d; ScribeFolder *Folder; FolderLeaf(FolderDlgPriv *priv, ScribeFolder *folder) { d = priv; Folder = folder; } bool IsSelectable() { auto fType = Folder ? Folder->GetItemType() : 0; return d->LimitTo == MAGIC_NONE || d->LimitTo == MAGIC_ANY || (Folder && d->LimitTo == fType); } const char *GetText(int i=0) override { return (Folder) ? Folder->GetText(i) : ""; } int GetImage(int Flags = 0) override { if (IsSelectable()) { return (Flags) ? ICON_OPEN_FOLDER : ICON_CLOSED_FOLDER; } return ICON_DISABLED_FOLDER; } ScribeFolder *GetFolder() { return Folder; } void OnSelect() override { LViewI *Parent = Tree->LView::GetParent(); if (Parent) { Parent->SetCtrlEnabled(IDOK, IsSelectable()); Parent->SetCtrlEnabled(IDC_NEW_FOLDER, IsSelectable() && d->CreateNew); } } }; void FolderDlgPriv::OnFilter() { // Initialize visible View->ForAllItems([vis = Filter ? LCss::DispNone : LCss::DispBlock](auto i) { FolderLeaf *l = dynamic_cast(i); if (!l) return; l->GetCss(true)->Display(vis); }); // Find matching items... View->ForAllItems([this](auto i) { FolderLeaf *l = dynamic_cast(i); if (!l) return; auto nm = l->GetText(); if (Stristr(nm, this->Filter.Get())) { l->GetCss(true)->Display(LCss::DispBlock); for (auto p = l->GetParent(); p; p = p->GetParent()) { FolderLeaf *lp = dynamic_cast(p); if (lp) lp->GetCss(true)->Display(LCss::DispBlock); } } }); View->UpdateAllItems(); View->Invalidate(); } ////////////////////////////////////////////////////////////////////////////// class LFolderCtrlFactory : public LViewFactory { LView *NewView(const char *Class, LRect *Pos, const char *Text) { if (!Stricmp(Class, "ScribeFolderTree")) return new ScribeFolderTree(-1, 0, 0, 100, 100); return 0; } } FolderCtrlFactory; ScribeFolderTree::ScribeFolderTree(int id, int x, int y, int cx, int cy) : LTree(id, x, y, cx, cy, "") { - _ObjName = Res_Custom; + SetObjectName(Res_Custom); Sunken(true); AskImage(true); } void ScribeFolderTree::Setup(FolderDlgPriv *d, ScribeFolder *Root, const char *InitSel) { if (!Root) return; FolderLeaf *Leaf = new FolderLeaf(d, Root); if (Leaf) { Insert(Leaf); AddFolder(d, Leaf, Root); ScribeFolder *f = Root; while ((f = f->GetNextFolder())) { Leaf = new FolderLeaf(d, f); if (Leaf) { Insert(Leaf); AddFolder(d, Leaf, f); } } if (InitSel) { LToken Path(InitSel, "/"); LTreeNode *n = this; for (unsigned i=0; i(n->GetChild()); c; c = dynamic_cast(c->GetNext())) { auto s = c->GetFolder()->GetName(true); if (s.Equals(Path[i])) { Match = c; break; } } if (Match) n = Match; else break; } LTreeItem *it = dynamic_cast(n); if (it) it->Select(true); } } } void ScribeFolderTree::AddFolder(FolderDlgPriv *d, LTreeItem *i, ScribeFolder *f) { if (i && f) { for (ScribeFolder *Folder = f->GetChildFolder(); Folder; Folder = Folder->GetNextFolder()) { FolderLeaf *Leaf = new FolderLeaf(d, Folder); if (Leaf) { i->Insert(Leaf); AddFolder(d, Leaf, Folder); i->Expanded(true); } } } } LString ScribeFolderTree::Get2() { LString a; FolderLeaf *Leaf = dynamic_cast(Selection()); if (Leaf) { ScribeFolder *Folder = Leaf->GetFolder(); if (Folder) a = Folder->GetPath(); } return a; } ////////////////////////////////////////////////////////////////////////////// FolderDlg::FolderDlg( LViewI *parent, ScribeWnd *app, int LimitToType, ScribeFolder *Root, const char *InitialSelect, bool AllowCreate, char *DefaultNewFolderName, char *DialogMsg) { d = new FolderDlgPriv(app, this, AllowCreate); d->DefaultNewFolderName = DefaultNewFolderName; d->LimitTo = LimitToType; SetParent(parent); if (LoadFromResource(IDD_FOLDER_SELECT)) { MoveToCenter(); if (GetViewById(IDC_FOLDERS, d->View)) { if (!Root) { LMailStore *Def = d->App->GetDefaultMailStore(); if (Def) { Root = Def->Root; while (Root->GetPrev()) { Root = dynamic_cast(Root->GetPrev()); } } } d->View->Setup(d, Root, InitialSelect); d->View->SetImageList(d->App->GetIconImgList(), false); } LViewI *msg; if (DialogMsg && GetViewById(IDC_FOLDER_MSG, msg)) { msg->Name(DialogMsg); } } SetCtrlEnabled(IDOK, false); SetCtrlEnabled(IDC_NEW_FOLDER, false); } FolderDlg::~FolderDlg() { DeleteObj(d); } char *FolderDlg::Get() { return d->Path; } int FolderDlg::OnNotify(LViewI *Ctrl, LNotification n) { switch (Ctrl->GetId()) { case IDC_FOLDERS: { if (n.Type == LNotifyItemDoubleClick) { if (d->View) d->Path = d->View->Get2(); EndModal(1); } break; } case IDC_NEW_FOLDER: { auto Cur = d->View->Get2(); if (!Cur) break; Store3ItemTypes Type[] = { MAGIC_MAIL, MAGIC_CONTACT, MAGIC_FILTER, MAGIC_CALENDAR, MAGIC_GROUP }; bool Enable[] = {true, true, true, true, true}; CreateSubFolderDlg Dlg(this, 0, Enable, d->DefaultNewFolderName); ScribeFolder *f = d->App->GetFolder(Cur); if (!f) break; ScribeFolder *Sub = f->GetSubFolder(Dlg.SubName); if (!Sub) Sub = f->CreateSubDirectory(Dlg.SubName, Type[Dlg.SubType]); if (!Sub) break; LTreeItem *s = d->View->Selection(); if (!s) break; FolderLeaf *NewLeaf = new FolderLeaf(d, Sub); s->Insert(NewLeaf); NewLeaf->Select(true); NewLeaf->ScrollTo(); break; } case IDC_FILTER: { LString n = Ctrl->Name(); if (d->Filter != n) { d->Filter = n; d->OnFilter(); } break; } case IDC_CLEAR: { d->Filter.Empty(); SetCtrlName(IDC_FILTER, NULL); d->OnFilter(); break; } case IDOK: { if (d->View) d->Path = d->View->Get2(); EndModal(1); break; } case IDCANCEL: { EndModal(0); break; } } return 0; } diff --git a/Code/Store3Imap/ScribeImap_Store.cpp b/Code/Store3Imap/ScribeImap_Store.cpp --- a/Code/Store3Imap/ScribeImap_Store.cpp +++ b/Code/Store3Imap/ScribeImap_Store.cpp @@ -1,1311 +1,1313 @@ #include "lgi/common/Lgi.h" #include "lgi/common/NetTools.h" #include "ScribeImap.h" #include "ScribeUtils.h" #if IMAP_PROTOBUF #ifdef _DEBUG #pragma comment(lib, "libprotobufd.lib") #else #pragma comment(lib, "libprotobufd.lib") #endif #endif const char *ImapMsgTypeNames[IMAP_MSG_MAX] = { "IMAP_NULL", // Connection "IMAP_ONLINE", "IMAP_OFFLINE", // 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", // Mail "IMAP_FOLDER_SELECTED", "IMAP_FOLDER_LISTING", "IMAP_DOWNLOAD", "IMAP_APPEND", "IMAP_MOVE_EMAIL", // General "IMAP_DELETE", "IMAP_UNDELETE", "IMAP_SET_FLAGS", }; ////////////////////////////////////////////////////////////////////////////////// char *TrimWhite(char *c) { if (!c) return 0; char *s = c; while (*s && strchr(WhiteSpace, *s)) s++; char *e = s + strlen(s); while (e > s && strchr(WhiteSpace, e[-1])) e--; *e = 0; if (s > c) memmove(c, s, e-s+1); return c; } ////////////////////////////////////////////////////////////////////////////////// ImapStore::ImapStore(char *host, int port, char *user, char *pass, int flags, LDataEventsI *callback, LCapabilityClient *caps, MailProtocolProgress *prog[2], LStream *log, int accountid, LAutoPtr store) { Root = NULL; Cache = 0; Online = STATUS_OFFLINE; LastPulse = 0; ListingTime = SelectTime = 0; AccountId = accountid; Host = host; Port = port; User = user; Pass = pass; ConnectFlags = flags; Log = log; Callback = callback; ItemProgress = prog[0]; DataProgress = prog[1]; SettingStore = store; Thread = new ImapThread(this, caps, log, SettingStore); } ImapStore::~ImapStore() { FolderLoader.Reset(); DeleteObj(Thread); DeleteObj(Root); DeleteArray(Cache); } bool ImapStore::OnIdle() { if (Listing.Length() > 0) { LAutoPtr m(Listing[0]); Listing.DeleteAt(0, true); return Listing.Length() > 0; } int64 Now = LCurrentTime(); if (!LastPulse || (Now - LastPulse) > TIMEOUT_STORE_ONPULSE) { LastPulse = Now; if (Root) Root->OnPulse(); } return false; } const char *ImapStore::GetStr(int id) { switch (id) { case FIELD_ERROR: return ErrorMsg; + case FIELD_STORE_TYPE: + return "ImapStore"; } return NULL; } int64 ImapStore::GetInt(int id) { switch (id) { case FIELD_STORE_TYPE: return Store3Imap; case FIELD_VERSION: return 10; case FIELD_ACCOUNT_ID: return AccountId; case FIELD_IS_ONLINE: return Online == STATUS_ONLINE; case FIELD_PROFILE_IMAP_LISTING: return ListingTime; case FIELD_PROFILE_IMAP_SELECT: return SelectTime; case FIELD_STORE_STATUS: return Online; } return -1; } Store3Status ImapStore::SetInt(int id, int64 val) { if (id == FIELD_IS_ONLINE && !val) { DeleteObj(Thread); Online = STATUS_OFFLINE; return Store3Success; } return Store3Error; } ImapFolder *ImapStore::GetSystemFolder(int Type) { if (!Root) return NULL; Store3SystemFolder t = Store3SystemNone; switch (Type) { case FOLDER_INBOX: t = Store3SystemInbox; break; case FOLDER_OUTBOX: t = Store3SystemOutbox; break; case FOLDER_SENT: t = Store3SystemSent; break; case FOLDER_TRASH: t = Store3SystemTrash; break; } if (!t) return NULL; for (auto f: Root->Sub.a) { if (f->System == t) return f; } return NULL; } LDataPropI *ImapStore::GetObj(int id) { switch (id) { case FIELD_INBOX: return GetSystemFolder(FOLDER_INBOX); case FIELD_OUTBOX: return GetSystemFolder(FOLDER_OUTBOX); case FIELD_SENT: return GetSystemFolder(FOLDER_SENT); case FIELD_TRASH: return GetSystemFolder(FOLDER_TRASH); } return NULL; } void ImapStore::OnNew(const char *File, int Line, LDataFolderI *parent, LArray &new_items, int pos, bool is_new) { if (!Callback) return; Callback->SetContext(File, Line); Callback->OnNew(parent, new_items, pos, is_new); } bool ImapStore::OnDelete(const char *File, int Line, LDataFolderI *parent, LArray &del) { if (!Callback) return false; Callback->SetContext(File, Line); return Callback->OnDelete(parent, del); } bool ImapStore::OnChange(const char *File, int Line, LArray &items, int FieldHint) { if (!Callback) return false; Callback->SetContext(File, Line); return Callback->OnChange(items, FieldHint); } static bool CheckCreateFolder(char *s, LStream *Log) { if (LDirExists(s)) return true; if (FileDev->CreateFolder(s)) return true; char m[256]; Log->Write(m, sprintf_s(m, sizeof(m), "Can't create '%s'", s), LSocketI::SocketMsgError); return false; } char *ImapStore::GetCache() { if (!Cache) { char s[MAX_PATH_LEN], a[32]; LVariant Portable; LOptionsFile *Opts = Callback->GetOptions(); if (!Opts || !Opts->GetValue(OPT_IsPortableInstall, Portable)) Portable = true; sprintf_s(a, sizeof(a), "%i", AccountId); if (Portable.CastInt32()) LGetSystemPath(LSP_APP_INSTALL, s, sizeof(s)); else LGetSystemPath(LSP_APP_ROOT, s, sizeof(s)); if (!CheckCreateFolder(s, Log)) { LgiTrace("%s:%i - CheckCreateFolder(%s) failed.\n", _FL, s); return NULL; } LMakePath(s, sizeof(s), s, "ImapCache"); if (!CheckCreateFolder(s, Log)) { LgiTrace("%s:%i - CheckCreateFolder(%s) failed.\n", _FL, s); return NULL; } LMakePath(s, sizeof(s), s, a); if (!CheckCreateFolder(s, Log)) { LgiTrace("%s:%i - CheckCreateFolder(%s) failed.\n", _FL, s); return NULL; } Cache = NewStr(s); } return Cache; } uint64 ImapStore::Size() { return -1; } LDataI *ImapStore::Create(int Type) { switch ((uint32_t)Type) { case MAGIC_FOLDER: { return new ImapFolder(this, 0); } case MAGIC_MAIL: { return new ImapMail(this, _FL, 0); } } return 0; } class FolderLoaderThread : public LThread { bool Loop; ImapStore *Store; public: uint64_t StartTs; LAutoPtr Root; FolderLoaderThread(ImapStore *store, const char *base) : LThread("FolderLoaderThread") { Loop = true; Store = store; StartTs = LCurrentTime(); Root.Reset(new ImapFolder(Store, "/")); Run(); } ~FolderLoaderThread() { Loop = false; while (!IsExited()) LSleep(1); } int Main() { Root->LoadSub(true); Store->PostStore(new ImapMsg(IMAP_ONLINE, _FL)); return 0; } }; LDataFolderI *ImapStore::GetRoot(bool create) { if (!GetCache()) return NULL; if (!Root) { Root = new ImapFolder(this, "/"); #if 0 // Start a thread to load the folder tree.. FolderLoader.Reset(new FolderLoaderThread(this, GetCache())); #endif } return Root; } ImapFolder *ImapStore::GetTrash() { ImapFolder *Trash = Root->Find(0, "/Trash"); if (!Trash) Trash = Root->Find(0, "/Deleted Items"); // Should we create a trash folder if it doesn't exist? return Trash; } Store3Status ImapStore::Delete(LArray &Items, bool ToTrash) { if (Items.Length() == 0) return Store3Error; Store3Status Status = Store3Error; LArray Mv, Dl; ImapFolder *Trash = GetTrash(); LAutoPtr DelMsg(new ImapMsg(IMAP_DELETE, _FL)); for (unsigned n=0; n(Items[n]); if (f) { Status = f->Delete(); } else { ImapMail *m = dynamic_cast(Items[n]); if (m) { if (ToTrash && Root) { if (Trash && m->Parent != Trash) { Mv.Add(m); m = NULL; } } if (m) { if (!DelMsg->Parent) DelMsg->Parent = m->Parent->Remote.Get(); // This breaks messages up into blocks of the same parent folder // but no more than IMAP_BLOCK items. if (_stricmp(DelMsg->Parent, m->Parent->Remote) || DelMsg->Mail.Length() >= IMAP_BLOCK_SIZE) { PostThread(DelMsg.Release(), false); DelMsg.Reset(new ImapMsg(IMAP_DELETE, _FL)); DelMsg->Parent = m->Parent->Remote.Get(); Status = Store3Delayed; } Dl.Add(m); m->SetState(ImapMail::ImapMailDeleting); DelMsg->Mail.New().Uid = m->Uid; } } } } if (Trash && Mv.Length()) { Status = Move(Trash, Mv); } if (DelMsg->Mail.Length()) { if (PostThread(DelMsg.Release(), false)) { Status = Store3Delayed; OnChange(_FL, Dl, 0); } } return Status; } Store3Status ImapStore::Move(LDataFolderI *NewFolder, LArray &Items) { Store3Status Status = Store3Error; if (NewFolder) { ImapFolder *To = CastFld(NewFolder); if (To) { Status = To->Move(Items); } else LAssert(!"Not an imap folder."); } return Status; } Store3Status ImapStore::Change(LArray &Items, int PropId, LVariant &Value, LOperator Operator) { switch (PropId) { case FIELD_FLAGS: { uint32_t Val = Value.CastInt32(); LAutoPtr Msg; if (Operator != OpPlusEquals && Operator != OpMinusEquals) { LAssert(!"Operator not supported."); return Store3Error; } for (auto i: Items) { if (!Msg && !Msg.Reset(new ImapMsg(IMAP_SET_FLAGS, _FL))) return Store3Error; ImapMail *m = dynamic_cast(i); if (!m || !m->Parent) continue; auto Fld = m->Parent->Remote; if (Msg->Parent && (Fld != Msg->Parent || Msg->Mail.Length() >= IMAP_BLOCK_SIZE)) { LAssert(Msg->Parent); PostThread(Msg.Release(), false); if (!Msg.Reset(new ImapMsg(IMAP_SET_FLAGS, _FL))) return Store3Error; } if (!Msg->Parent) Msg->Parent = Fld; auto PrevFlags = m->RemoteFlags.All; if (Val & MAIL_READ) m->RemoteFlags.ImapSeen = Operator == OpPlusEquals; else if (Val & MAIL_REPLIED) m->RemoteFlags.ImapAnswered = Operator == OpPlusEquals; else LAssert(!"Unsupported flag."); if (Operator == OpPlusEquals) m->LocalFlags |= Val; else m->LocalFlags &= ~Val; auto t = m->GetMeta(); if (t) { #if IMAP_PROTOBUF t->set_remoteflags(m->RemoteFlags.All); t->set_localflags(m->LocalFlags); #else t->SetAttr(ATTR_FLAGS, m->RemoteFlags.Get()); t->SetAttr(ATTR_LOCAL, m->LocalFlags); #endif m->Parent->SetDirty(); } else LAssert(!"No tag."); if (m->RemoteFlags.All != PrevFlags) { ImapMailInfo &Info = Msg->Mail.New(); Info.Uid = m->Uid; Info.Flags = m->RemoteFlags; } } if (Msg && Msg->Mail.Length() > 0) { LAssert(Msg->Parent); if (!PostThread(Msg.Release(), false)) return Store3Error; } return Store3Delayed; break; } } return Store3Error; } void ImapStore::Compact(LViewI *Parent, LDataPropI *Props, std::function OnStatus) { if (OnStatus) OnStatus(true); } bool ImapStore::PostThread(ImapMsg *m, bool UiPriority) { if (Thread) { Thread->PostThread(m, UiPriority); return true; } // This happens when the imap store is in offline mode. DeleteObj(m); return false; } ImapFolder *ImapStore::GetParent(char *Local, char *Remote) { if (!Root) return 0; LAutoString l(NewStr(Local)), r(NewStr(Remote)); char *s; if (l && (s = strrchr(l, DIR_CHAR))) *s = 0; if (r && (s = strrchr(r, '/'))) *s = 0; return Root->Find(l, r); } void MailInfToObject(LAutoPtr &DownloadMsg, LArray *OnNew, ImapFolder *Parent, ImapMail *o, ImapMailInfo &Inf) { o->Path = Parent->MailPath(Inf.Uid, false); if (Inf.Date) { o->DateSent.Decode(Inf.Date); o->DateSent.ToUtc(); ValidateImapDate(o->DateSent); } // LgiTrace("%s:%i - %p.DateSent = %s\n", _FL, o, Inf.Date.Get()); o->SetRemoteFlags(Inf.Flags); // LgiTrace("MailInfToObject %p -> %s / %s\n", o, Inf.Flags.Get(), EmailFlagsToStr(o->LocalFlags).Get()); o->Parent = Parent; if (o->RemoteFlags.ImapRecent || !LFileExists(o->Path)) { if (!DownloadMsg) { if (DownloadMsg.Reset(new ImapMsg(IMAP_DOWNLOAD, _FL))) DownloadMsg->Parent = Parent->Remote.Get(); } if (DownloadMsg) { ImapMailInfo &Info = DownloadMsg->Mail.New(); Info.Uid = o->Uid; Info.Local = o->Path.Get(); o->SetState(ImapMail::ImapMailGettingBody); } } Parent->WhenLoaded([Parent, o, OnNew](auto Status) { Parent->AddMail(o, Status == Store3Success ? OnNew : NULL); }); } static bool _in_download = false; void ImapStore::OnEvent(void *Param) { if (!Root) return; Msgs.Add((ImapMsg*)Param); while (Msgs.Length()) { int64 Start = LCurrentTime(); ImapMsg *m = Msgs[0]; Msgs.DeleteAt(0, true); switch (m->Type) { default: break; case IMAP_ONLINE: { Online = STATUS_ONLINE; // LProfile p("IMAP_ONLINE"); #if 1 if (Root) Root->LoadSub(); #else if (Root && Root->Sub.GetState() < Store3Loaded) { if (!FolderLoader) { // p.Add("CreateThread"); FolderLoader.Reset(new FolderLoaderThread(this, GetCache())); } else { FolderLoaderThread *t = dynamic_cast(FolderLoader.Get()); if (t && t->Root && t->Root->Sub.GetState() == Store3Loaded) { // p.Add("RootSwap"); Root->Swap(*t->Root); // LgiTrace("FolderLoadTime=" LPrintfInt64 "\n", LCurrentTime() - t->StartTs); } } } #endif // Select the inbox if (Root && Root->Sub.GetState() == Store3Loaded) { // p.Add("Inbox"); // Select the INBOX and start loading it's mail ImapFolder *Inbox = Root->Find(NULL, "/INBOX"); if (Inbox) { // p.Add("Inbox.Select"); Inbox->OnSelect(true); // p.Add("Inbox.Load"); Inbox->LoadMail(); } // p.Add("Callback"); if (Callback) Callback->OnPropChange(this, FIELD_IS_ONLINE, GV_BOOL); } break; } case IMAP_OFFLINE: { Online = STATUS_OFFLINE; if (Callback) Callback->OnPropChange(this, FIELD_IS_ONLINE, GV_BOOL); break; } case IMAP_ERROR: { Online = STATUS_ERROR; break; } case IMAP_ON_NEW: { if (!Root) break; // New folders? LArray NewEvent; for (unsigned i=0; iFld.Length(); i++) { ImapFolderInfo &Inf = m->Fld[i]; ImapFolder *Parent = GetParent(Inf.Local, 0); if (!Parent) { // This happens when there is "Inbox/abc/def" but no // "Inbox/abc". Apparently this is legal for some servers. char sep[] = {Inf.Sep, 0}; LString Sep(sep); LString Unix("/"); LAssert(Inf.Sep != 0); LString::Array a = LString(Inf.Remote).Split(Sep); Parent = Root; for (unsigned i=0; Parent && iFind(NULL, Rem); if (!f) { // Start creating entries... f = new ImapFolder(this); if (!f) break; f->Remote = Rem.Get(); char s[MAX_PATH_LEN]; LMakePath(s, sizeof(s), GetCache(), Rem); f->Local = s; if (!LDirExists(f->Local) && FileDev->CreateFolder(f->Local)) { // Probably should get the server to recreate the folder too? ImapMsg *Create = new ImapMsg(IMAP_CREATE_FOLDER, _FL); if (Create) { ImapFolderInfo &Inf = Create->Fld.New(); Inf.Local = f->Local.Get(); Inf.Remote = f->Remote.Get(); PostThread(Create, false); } } f->SetParent(Parent); NewEvent.Add(f); f->Field.State = Store3Loaded; } if (!f) break; Parent = f; } } if (Parent) { if (!LDirExists(Inf.Local)) { // Create local folder... if (FileDev->CreateFolder(Inf.Local)) { if (Parent->Sub.GetState() != Store3Loaded) { // This will cause the folder to be loaded as if it was pre-existing Parent->LoadSub(); ImapFolder *f = Parent->Find(Inf.Local, NULL); // Get the new folder ptr and pass it to the UI level if (f && Callback) NewEvent.Add(f); } else { LString RemotePath = Parent->MakeImapPath(Inf.Local); ImapFolder *f = new ImapFolder(this, RemotePath); if (f) { f->SetParent(Parent); // LgiTrace("IMAP Store '%s', parent state=%i\n", f->Remote.Get(), Parent->Sub.State); if (Callback) NewEvent.Add(f); } else if (Log) Log->Print("IMAP_ON_NEW: mem alloc failed\n"); } } else { LAssert(!"Failed to create folder."); if (Log) Log->Print("IMAP_ON_NEW: Failed to create folder\n"); } } } } for (unsigned i=0; i Items; Items.Add(NewEvent[i]); ImapFolder *Parent = NewEvent[i]->GetParent(); if (Parent->Sub.State != Store3Loaded) Parent->LoadSub(); Callback->OnNew(Parent, Items, -1, false); } // New email? ImapFolder *Parent = m->Parent ? Root->Find(0, m->Parent) : 0; if (Parent) { LArray Old, Recent; LAutoPtr DownloadMsg; ImapMail *o; for (unsigned i=0; iMail.Length(); i++) { ImapMailInfo &Inf = m->Mail[i]; if (!Inf.Uid) { LAssert(!"No server id."); continue; } bool ParentHasUid = Parent->GetMail(Inf.Uid) != NULL; if (!Inf.Flags.ImapDeleted && !ParentHasUid && (o = new ImapMail(this, _FL, Inf.Uid))) { LArray *a = Inf.Flags.ImapRecent && !Inf.Flags.ImapSeen && Parent->System == Store3SystemInbox ? &Recent : &Old; MailInfToObject(DownloadMsg, a, Parent, o, Inf); } } if (DownloadMsg) { PostThread(DownloadMsg.Release(), false); } if (Callback) { if (Recent.Length()) Callback->OnNew(Parent, Recent, -1, true); if (Old.Length()) Callback->OnNew(Parent, Old, -1, false); } } break; } case IMAP_ON_DEL: { if (!Root) { LAssert(!"No root?"); break; } LgiTrace("IMAP_ON_DEL %i,%i\n", (int)m->Fld.Length(), (int)m->Mail.Length()); // Folders for (unsigned i=0; iFld.Length(); i++) { ImapFolderInfo &Inf = m->Fld[i]; ImapFolder *f = Root->Find(Inf.Local, Inf.Remote); if (f) { LArray Lst; Lst.Add(f); if (Callback->OnDelete(f->GetParent(), Lst)) { f->OnDeleteComplete(); DeleteObj(f); } else LAssert(!"OnDelete failed."); } } // Email ImapFolder *Parent = m->Parent ? Root->Find(0, m->Parent) : 0; if (Parent) { LArray Lst; if (m->Mail.Length() > 0) { for (unsigned i=0; iMail.Length(); i++) { ImapMailInfo &Inf = m->Mail[i]; ImapMail *e = Parent->GetMail(Inf.Uid); if (!e) { LgiTrace("\t%s:%i - Can't find uid '%i' in folder '%s' (msg from %s:%i)\n", _FL, Inf.Uid, m->Parent.Get(), m->File, m->Line); // LAssert(!"No email to delete."); } else if (!Lst.HasItem(e)) { // LgiTrace("%s:%i - Adding %p %s to del lst.\n", _FL, e, e->Uid.Get()); Lst.Add(e); } } } else { for (unsigned i=0; iChildren().Length(); i++) { LDataI *di = Parent->Children()[i]; if (di) Lst.Add(di); } } LgiTrace("\tCallback->OnDelete %i\n", (int)Lst.Length()); if (!Callback || Callback->OnDelete(Parent, Lst)) { for (unsigned i=0; i(Lst[i]); if (e) { #if 0 LgiTrace("\t%s:%i - OnDelete '%s' from folder '%s' (msg from %s:%i)\n", _FL, e->Uid.Get(), Parent->Remote.Get(), m->File, m->Line); #endif LAssert(!_in_download); e->OnDelete(); DeleteObj(e); } } } } // LgiTrace("Ending IMAP_ON_DEL\n"); break; } case IMAP_FOLDER_LISTING: { if (m->Fld.Length() != 1) { LAssert(!"Not the right folder length."); break; } ImapFolder *f = Root->Find(0, m->Fld[0].Remote); if (f) f->OnListing(m); else LAssert(!"No folder"); break; } case IMAP_DOWNLOAD: { ImapFolder *f = Root->Find(0, m->Parent); if (f) { _in_download = true; LArray Changed; for (unsigned i=0; iMail.Length(); i++) { ImapMailInfo &Inf = m->Mail[i]; ImapMail *e = f->GetMail(Inf.Uid); if (e) { e->SetState(ImapMail::ImapMailIdle); auto t = e->GetMeta(); if (t) { e->ReadMime(t); Changed.Add(e); } else { f->Save(); LgiTrace("Error: Can't find meta data for email %s\n", strrchr(e->Path, DIR_CHAR)); LAssert(!"No meta tag for this email?"); } } else { // That can happen if you delete something before the IMAP_DOWNLOAD msg // gets back from the worker thread. } } _in_download = false; f->SetDirty(); if (Changed.Length()) OnChange(_FL, Changed, 0); } else LAssert(!"No folder"); break; } case IMAP_CREATE_FOLDER: { if (Callback && Root) { for (unsigned i=0; iFld.Length(); i++) { ImapFolderInfo &Inf = m->Fld[i]; ImapFolder *f = Root->Find(Inf.Local, Inf.Remote); if (f) { LArray New; New.Add(f); Callback->OnNew(f->GetParent(), New, -1, false); } } } break; } case IMAP_APPEND: { LDataFolderI *NewItemParent = 0; LArray NewItems; ImapFolder *f = Root->Find(0, m->Parent); if (f) { for (unsigned i=0; iMail.Length(); i++) { ImapMailInfo &Inf = m->Mail[i]; ImapMail *m = f->FindByFile(Inf.Local); if (m) { if (Inf.Uid) { // Ok rename the mail object, setup it's new UID m->Uid = Inf.Uid; LString Path = f->MailPath(m->Uid); if (FileDev->Move(m->Path, Path)) { m->Path = Path; } // Create the meta data tag and hook it up auto t = f->GetMeta(m->Uid, true); if (t) { LAssert(m->GetMeta() != NULL); m->Serialize(t, true); if (NewItemParent != NULL && NewItemParent != m->Parent) { // If items from different parents come through we should OnNew them // only when the parent is the same, so flush through ones that have // the old parent folder before we start collecting ones from the new // folder.. if (Callback && NewItems.Length() > 0) Callback->OnNew(NewItemParent, NewItems, -1, false); NewItems.Length(0); } NewItemParent = m->Parent; NewItems.Add(m); if (!m->Seg) { auto Meta = m->GetMeta(); m->Loaded = Store3Unloaded; m->ReadMime(Meta); } } } else { // No uid, just destroy it, we'll get it eventually through the // \recent mail search FileDev->Delete(m->Path, false); m->Path.Empty(); } } } } if (Callback && NewItems.Length()) { // Notify the client the object has arrived. Callback->OnNew(NewItemParent, NewItems, -1, false); } break; } case IMAP_EXPUNGE_FOLDER: { ImapFolder *f = Root->Find(0, m->Parent); if (f) f->OnExpunge(); break; } case IMAP_RENAME_FOLDER: { LArray Renamed; ImapFolder *f = Root->Find(0, m->Parent); if (f && f->OnRename(m->NewRemote)) Renamed.Add(f); OnChange(_FL, Renamed, FIELD_FOLDER_NAME); break; } case IMAP_SET_FLAGS: { LArray Change, Delete, OnNew; ImapFolder *f = Root->Find(0, m->Parent ); if (!f) { LgiTrace("%s:%i - Missing folder '%s'\n", _FL, m->Parent.Get()); break; } LAutoPtr DownloadMsg; #define USE_BINARY_SEARCH 1 // LProfile prof("IMAP_SET_FLAGS"); #if USE_BINARY_SEARCH // Sort the list for BinarySearch //prof.Add("sort"); f->Mail.a.Sort([](auto a, auto b) { return (int32_t)(*a)->Uid - (int32_t)(*b)->Uid; }); #endif // prof.Add("process"); for (auto &e: m->Mail) { if (e.Uid) { #if USE_BINARY_SEARCH ImapMail *Email = f->Mail.a.BinarySearch([Uid=e.Uid](auto a) { return (int32_t)a->Uid - (int32_t)Uid; }); #else // On large folders this is sooooo slow... ImapMail *Email = f->GetMail(e.Uid); #endif // This is causing deleted email to reappear?? if (!Email && !e.Flags.ImapDeleted) { // Existing email that we don't know about? // Ok, well add new entry to track it. ImapMail *NewMail = new ImapMail(this, _FL, e.Uid); if (NewMail) MailInfToObject(DownloadMsg, &OnNew, f, NewMail, e); } if (Email) { if (Email->RemoteFlags.ImapDeleted ^ e.Flags.ImapDeleted) { if (e.Flags.ImapDeleted) Delete.Add(Email); // LAssert(!"What now?"); } else { Change.Add(Email); } Email->SetRemoteFlags(e.Flags); } } else { LgiTrace("%s:%i - No UID: seq=%i\n", _FL, e.Seq); } } if (OnNew.Length()) { f->SetDirty(); Callback->OnNew(f, OnNew, -1, true); } if (Change.Length()) { f->SetDirty(); OnChange(_FL, Change, FIELD_FLAGS); } else if (Delete.Length()) { f->SetDirty(); Callback->OnDelete(f, Delete); } if (DownloadMsg) { PostThread(DownloadMsg.Release(), false); } break; } case IMAP_LOAD_FOLDER: { // This event comes from the delayed ImapFolder::LoadMail thread... NOT the imap worker thread. if (!m->Fld.Length()) { LAssert(!"Must have 1 entry."); break; } ImapFolder *f = Root->Find(m->Fld[0].Local, NULL); if (!f) { LAssert(!"Not an IMAP folder."); break; } f->OnLoadMail(true); break; } } int Time = (int) (LCurrentTime() - Start); if (Time > 1000) { LgiTrace("%s:%i - ImapStore::OnEvent took %ims processing msg %s (len=%i)\n", _FL, Time, m && m->Type < IMAP_MSG_MAX ? ImapMsgTypeNames[m->Type] : "", m ? m->Mail.Length() : 0); } DeleteObj(m); } } //////////////////////////////////////////////////////////////////////////////////////////////// bool ValidateImapDate(LDateTime &dt) { if (dt.Year() == 2000 && dt.Hours() == 0) { LAssert(!"Invalid IMAP date."); dt.Empty(); return false; } if (dt.GetTimeZone() != 0) { LgiTrace("%s:%i - Should be a UTC date.\n", _FL); return false; } return true; } LDataStoreI *OpenImap( char *Host, int Port, char *User, char *Pass, int ConnectFlags, LDataEventsI *Callback, LCapabilityClient *Caps, MailProtocolProgress *prog[2], LStream *Log, int AccoundId, LAutoPtr store) { return new ImapStore(Host, Port, User, Pass, ConnectFlags, Callback, Caps, prog, Log, AccoundId, store); } diff --git a/Code/Store3Mail3/Mail3Attachment.cpp b/Code/Store3Mail3/Mail3Attachment.cpp --- a/Code/Store3Mail3/Mail3Attachment.cpp +++ b/Code/Store3Mail3/Mail3Attachment.cpp @@ -1,564 +1,568 @@ #include "Mail3.h" #include "lgi/common/TextConvert.h" Mail3BlobStream::Mail3BlobStream(LMail3Store *store, int segid, int size, const char *file, int line, bool Write) { Store = store; File = file; Line = line; b = 0; Pos = 0; Size = size; SegId = segid; WriteAccess = Write; #if MAIL3_TRACK_OBJS LMail3Store::SqliteObjs &_d = Store->All.New(); _d.Stream = this; #endif OpenBlob(); } Mail3BlobStream::~Mail3BlobStream() { CloseBlob(); #if MAIL3_TRACK_OBJS Store->RemoveFromAll(this); #endif } bool Mail3BlobStream::OpenBlob() { bool Res = Store->Check(sqlite3_blob_open(Store->GetDb(), NULL, // Database "MailSegs", "Data", SegId, WriteAccess, &b), 0); // if (b) LgiTrace("%s:%i - open blob %p\n", _FL, b); return Res; } bool Mail3BlobStream::CloseBlob() { if (!b) return true; // LgiTrace("%s:%i - close blob %p\n", _FL, b); int Res = sqlite3_blob_close(b); b = NULL; return Store->Check(Res, 0); } int64 Mail3BlobStream::GetPos() { return Pos; } int64 Mail3BlobStream::SetPos(int64 p) { if (p < 0) p = 0; if (p > Size) p = Size; return Pos = p; } int64 Mail3BlobStream::GetSize() { return Size; } int64 Mail3BlobStream::SetSize(int64 sz) { LAssert(!"You can't set the size of a blob."); return Size; } ssize_t Mail3BlobStream::Read(void *Buf, ssize_t Len, int Flags) { if (!b) return 0; int64 Remain = Size - Pos; int64 Copy = MIN(Remain, Len); auto Res = sqlite3_blob_read(b, Buf, (int)Copy, (int)Pos); if (Res == SQLITE_ABORT) { if (!CloseBlob()) return 0; if (!OpenBlob()) return 0; Res = sqlite3_blob_read(b, Buf, (int)Copy, (int)Pos); } if (!Store->Check(Res, 0)) return 0; Pos += Copy; return (ssize_t) Copy; } ssize_t Mail3BlobStream::Write(const void *Buf, ssize_t Len, int Flags) { if (!b) return 0; auto Res = sqlite3_blob_write(b, Buf, (int)Len, (int)Pos); if (Res == SQLITE_ABORT) { if (!CloseBlob()) return 0; if (!OpenBlob()) return 0; Res = sqlite3_blob_write(b, Buf, (int)Len, (int)Pos); } if (!Store->Check(Res, 0)) return 0; Pos += Len; return Len; } /////////////////////////////////////////////////////////////////////////////////////// LMail3Attachment::LMail3Attachment(LMail3Store *store) : Store3Attachment(store) { SegId = -1; BlobSize = 0; InMemoryOnly = false; } LMail3Attachment::~LMail3Attachment() { _Delete(); } void LMail3Attachment::SetInMemoryOnly(bool b) { InMemoryOnly = b; for (unsigned i=0; iSetInMemoryOnly(b); } } uint64 LMail3Attachment::Size() { int64 HeaderSz = Headers ? strlen(Headers) : 0; int64 NameSz = Name.Length(); int64 MimeSz = MimeType.Length(); int64 ContentIdSz = ContentId.Length(); int64 CharsetSz = Charset.Length(); int64 ImportSz = Import ? Import->GetSize() : 0; return HeaderSz + NameSz + MimeSz + ContentIdSz + CharsetSz + BlobSize + sizeof(BlobSize) + sizeof(SegId) + ImportSz; } uint64 LMail3Attachment::SizeChildren() { uint64 s = 0; for (unsigned i=0; iSize(); s += c->SizeChildren(); } return s; } LMail3Attachment *LMail3Attachment::Find(int64 Id) { if (SegId == Id) return this; for (unsigned i=0; iFind(Id); if (r) return r; } return 0; } Store3CopyImpl(LMail3Attachment) { Headers.Reset(NewStr(p.GetStr(FIELD_INTERNET_HEADER))); if (Headers) { // Source supports headers... ParseHeaders(); } else { // Copy over whatever fields we can... Name = p.GetStr(FIELD_NAME); MimeType = p.GetStr(FIELD_MIME_TYPE); ContentId = p.GetStr(FIELD_CONTENT_ID); Charset = p.GetStr(FIELD_CHARSET); } LDataI *Data = dynamic_cast(&p); if (Data) { LAutoStreamI tmp = Data->GetStream(_FL); SetStream(tmp); } return true; } char *LMail3Attachment::GetHeaders() { if (!Headers) { LStringPipe p; - LAssert(MimeType != NULL); + if (!MimeType) + { + LAssert(!"MimeType is required."); + return NULL; + } p.Print("Content-Type: %s", MimeType?MimeType.Get():(char*)"text/plain"); if (Charset) p.Print("; charset=%s", Charset.Get()); if (Name) p.Print("; name=\"%s\"", Name.Get()); p.Print("\r\n"); if (ContentId) { p.Print("Content-Id: <%s>\r\n", ContentId.Strip("<>").Get()); if (Name) p.Print("Content-Disposition: inline; filename=\"%s\"\r\n", Name.Get()); } Headers.Reset(p.NewStr()); } return Headers; } bool LMail3Attachment::ParseHeaders() { LAutoString Ct(InetGetHeaderField(Headers, "Content-Type")); char *Colon = Ct ? strchr(Ct, ';') : 0; if (Colon) { while (Colon > Ct.Get() && strchr(" \t\r\n", Colon[-1])) Colon--; LAutoString Cs(InetGetSubField(Colon, "charset")); if (Cs) { Charset = Cs.Get(); LAssert(!strchr(Charset, '>')); } *Colon = 0; } MimeType = Ct; return true; } bool LMail3Attachment::Load(LMail3Store::LStatement &s, int64 &ParentId) { SegId = s.GetInt64(0); ParentId = s.GetInt64(2); Headers.Reset(NewStr(s.GetStr(3))); ParseHeaders(); BlobSize = s.GetSize(4); Dirty = false; LAssert(!Mail || Kit == Mail->Store); return true; } Store3Status LMail3Attachment::Save(LDataI *NewParent) { if (NewParent) { // Check hierarchy LMail3Attachment *NewSeg = dynamic_cast(NewParent); if (NewSeg) { // This propagates the in mem only setting down the tree of nodes InMemoryOnly = NewSeg->InMemoryOnly; if (NewSeg != Parent) AttachTo(NewSeg); } else { LMail3Mail *NewMail = dynamic_cast(NewParent); if (NewMail) { if (NewMail != Mail) AttachTo(NewMail); } } } if (Mail) { // Mark the mail size dirty. Mail->MailSize = -1; LAssert(Kit == Mail->Store); } if (!InMemoryOnly) { if (Mail && Mail->Id > 0) { if (SegId <= 0) { LMail3Attachment *Parent = GetParent(); LMail3Store::LInsert Ins(Kit, MAIL3_TBL_MAILSEGS); Ins.SetInt64(1, Mail->Id); Ins.SetInt64(2, Parent ? Parent->SegId : -1); Ins.SetStr(3, GetHeaders()); if (Import) Ins.SetStream(4, "Data", Import); if (!Ins.Exec()) return Store3Error; SegId = Ins.LastInsertId(); } else if (Dirty) { LMail3Attachment *Parent = GetParent(); LMail3Store::LUpdate Up(Kit, MAIL3_TBL_MAILSEGS, SegId, Import ? 0 : (char*)"Data"); Up.SetInt64(0, SegId); Up.SetInt64(1, Mail->Id); Up.SetInt64(2, Parent ? Parent->SegId : -1); Up.SetStr(3, GetHeaders()); if (Import) Up.SetStream(4, "Data", Import); if (!Up.Exec()) return Store3Error; } Import.Reset(); Dirty = false; } else { Dirty = true; } } return Store3Success; } void LMail3Attachment::OnSave() { if (!Mail) { LAssert(!"Segment is not attached to a mail!"); return; } if (Dirty || SegId <= 0) { Save(); } for (unsigned i=0; iOnSave(); } } const char *LMail3Attachment::GetStr(int id) { switch (id) { case FIELD_CHARSET: { if (!Charset) { // Maybe a parent segment has a charset? for (LMail3Attachment *p = GetParent(); p; p = p->GetParent()) { auto Cs = p->GetStr(FIELD_CHARSET); if (Cs) return Cs; } } return Charset; } case FIELD_NAME: { if (!Name) { LAutoString t(InetGetHeaderField(Headers, "Content-Disposition")); LAutoString n(DecodeRfc2047(InetGetSubField(t, "filename"))); if (n) Name = n.Get(); else { LAutoString ct(InetGetHeaderField(Headers, "Content-Type")); if (ct) { if (n.Reset(DecodeRfc2047(InetGetSubField(ct, "name")))) Name = n.Get(); } } } return Name; break; } case FIELD_MIME_TYPE: { if (!MimeType) { LAutoString t(InetGetHeaderField(Headers, "Content-Type")); if (t) { char *c = strchr(t, ';'); if (c) { while (strchr(" \t\r\n", c[-1])) c--; MimeType.Set(t, c ? c - t : -1); } } } return MimeType; break; } case FIELD_CONTENT_ID: { if (!ContentId) { LAutoString Id(InetGetHeaderField(Headers, "Content-Id")); ContentId = LString(Id.Get()).Strip("<>"); } return ContentId; break; } case FIELD_INTERNET_HEADER: return Headers; } LAssert(!"Unknown id."); return NULL; } Store3Status LMail3Attachment::SetStr(int id, const char *str) { switch (id) { case FIELD_INTERNET_HEADER: Headers.Reset(NewStr(str)); ParseHeaders(); break; case FIELD_NAME: Name = str; Headers.Reset(); break; case FIELD_MIME_TYPE: MimeType = str; // FIXME: If we have headers from an incoming mail, this is an error to delete them here. LAssert(Headers.Get() == NULL); Headers.Reset(); break; case FIELD_CONTENT_ID: ContentId = LString(str).Strip("<>"); Headers.Reset(); break; case FIELD_CHARSET: Charset = str; if (Charset.Find(">") >= 0) LAssert(!"Invalid char"); Headers.Reset(); break; default: LAssert(!"Unknown id."); return Store3Error; } return Store3Success; } int64 LMail3Attachment::GetInt(int id) { switch (id) { case FIELD_STORE_TYPE: return Store3Sqlite; case FIELD_SIZE: return BlobSize; } LAssert(!"Unknown id."); return false; } Store3Status LMail3Attachment::SetInt(int id, int64 i) { LAssert(!"Unknown id."); return Store3Error; } Store3Status LMail3Attachment::Delete(bool ToTrash) { if (InMemoryOnly) return Store3Success; if (SegId <= 0) return Store3Error; LString Sql; Sql.Printf("delete from " MAIL3_TBL_MAILSEGS " where Id=" LPrintfInt64, SegId); LMail3Store::LStatement s(Kit, Sql); if (!s.Exec()) return Store3Error; SegId = -1; return Store3Success; } LAutoStreamI LMail3Attachment::GetStream(const char *file, int line) { LAutoStreamI Ret; if (Import) Ret.Reset(new LProxyStream(Import)); else if (SegId > 0 && BlobSize > 0) Ret.Reset(new Mail3BlobStream(Kit, (int)SegId, (int)BlobSize, file, line)); return Ret; } bool LMail3Attachment::SetStream(LAutoStreamI s) { Import = s; Dirty = true; BlobSize = Import ? Import->GetSize() : 0; if (Mail) Mail->ResetCaches(); return true; } 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); 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(); 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()) { 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 (!ValidStr(str)) + // There is no point continuing as it will just attach an empty + // attachment which eventually asserts in the save code. + // e.g. LMail3Attachment::GetHeaders() + break; + 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; } 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/Store3Mail3/Mail3Store.cpp b/Code/Store3Mail3/Mail3Store.cpp --- a/Code/Store3Mail3/Mail3Store.cpp +++ b/Code/Store3Mail3/Mail3Store.cpp @@ -1,2222 +1,2224 @@ #include "Mail3.h" #include "resdefs.h" #include "lgi/common/SubProcess.h" #include #include "lgi/common/LgiRes.h" #include "lgi/common/Thread.h" #include "lgi/common/Store3MimeTree.h" #define SanityCheck() if (!IsOk()) return NULL; /////////////////////////////////////////////////////////////////////////////////// LMail3Thing::~LMail3Thing() { if (Parent) Parent->Items.a.Delete(this); } Store3Status LMail3Thing::Delete(bool ToTrash) { LArray Del; Del.Add(this); return Store->Delete(Del, ToTrash); } Store3Status LMail3Thing::Save(LDataI *Folder) { LMail3Folder *Fld = dynamic_cast(Folder); if (Folder && !Fld) { LAssert(!"Not the right folder type"); LgiTrace("%s:%i - Not the right folder type.\n", _FL); return Store3Error; } if (Fld && Fld->Store != Store) { SetStore(Fld->Store); } const char *Table = GetTable(); bool Create = false; bool IsNewMail = Type() == MAGIC_MAIL && TestFlag(GetInt(FIELD_FLAGS), MAIL_NEW); LArray NewItems; if (Id < 0) { if (Fld) { // Make sure the folder is loaded... Fld->Children(); Parent = Fld; ParentId = Fld->Id; LAssert(Parent->Items.IndexOf(this, true) < 0); // LgiTrace("%s:%i - Saving %i to %s (%i items)\n", _FL, (int)Id, Parent->GetStr(FIELD_FOLDER_NAME), Parent->Items.Length()); Parent->Items.Insert(this, -1, true); Create = true; NewItems.Add(this); if (IsNewMail) SetInt(FIELD_FLAGS, GetInt(FIELD_FLAGS) & ~MAIL_NEW); } else { LAssert(!"No parent to save to."); LgiTrace("%s:%i - No parent to save to.\n", _FL); return Store3Error; } } else if (Fld) { // Make sure the folder is loaded... Fld->Children(); ParentId = Fld->Id; if (Parent != Fld) { LDataFolderI *Old = Parent; if (Parent) { // Remove this item from the current parent folder... LAssert(Parent->Items.IndexOf(this) >= 0); Parent->Items.Delete(this); } // Add it to the new one... Parent = Fld; LAssert(Parent->Items.IndexOf(this, true) < 0); // LgiTrace("%s:%i - Saving %i to %s (%i items)\n", _FL, (int)Id, Parent->GetStr(FIELD_FOLDER_NAME), Parent->Items.Length()); Parent->Items.Insert(this, -1, true); LArray a; a.Add(this); if (Old) Store->OnMove(_FL, Parent, Old, a); else NewItems.Add(this); if (IsNewMail) SetInt(FIELD_FLAGS, GetInt(FIELD_FLAGS) & ~MAIL_NEW); } } bool Status = Write(Table, Create); if (Status) { OnSave(); if (NewItems.Length()) Store->OnNew(_FL, Parent, NewItems, -1, IsNewMail); } else { LgiTrace("%s:%i - Write failed.\n", _FL); } return Status ? Store3Success : Store3Error; } /////////////////////////////////////////////////////////////////////////////////// GMail3Idx Mail3Indexes[] = { {"MailFolderIdx", MAIL3_TBL_MAIL, "ParentId"}, {"MailSegMailIdx", MAIL3_TBL_MAILSEGS, "MailId"}, {"MailSegParentIdx", MAIL3_TBL_MAILSEGS, "ParentId"}, }; LMail3Store::LMail3Store(const char *Mail3Folder, LDataEventsI *callback, bool Create) : TableStatus(0, Store3Error) { Format = Mail3v1; Folder = Mail3Folder; Callback = callback; Db = 0; Transaction = 0; Root = 0; OpenStatus = Store3Success; Fields.Add(MAIL3_TBL_FOLDER, TblFolder); Fields.Add(MAIL3_TBL_FOLDER_FLDS, TblFolderFlds); Fields.Add(MAIL3_TBL_MAIL, TblMail); Fields.Add(MAIL3_TBL_MAILSEGS, TblMailSegs); Fields.Add(MAIL3_TBL_CONTACT, TblContact); Fields.Add(MAIL3_TBL_GROUP, TblGroup); Fields.Add(MAIL3_TBL_FILTER, TblFilter); Fields.Add(MAIL3_TBL_CALENDAR, TblCalendar); bool Exist = LDirExists(Mail3Folder); if (!Create && !Exist) { ErrorMsg.Printf(LLoadString(IDS_ERROR_FILE_DOESNT_EXIST), Mail3Folder); } else if (!Exist && !FileDev->CreateFolder(Mail3Folder)) { ErrorMsg.Printf(LLoadString(IDS_ERROR_CANT_CREATE_FOLDER), Mail3Folder); } else { OpenDb(); } #if _DEBUG LMail3Mail m(this); Store3MimeTree Tree(&m, m.Seg); if (!Tree.UnitTests(this, &m)) { LgiTrace("%s:%i - Error: Store3MimeTree unit tests failed\n", _FL); LAssert(!"Store3MimeTree unit tests failed.\n"); } #endif } LMail3Store::~LMail3Store() { CloseDb(); DeleteObj(Root); } LMail3Folder *LMail3Store::GetSystemFolder(int Type) { if (!Callback || !Root) return NULL; LVariant Path; if (Callback->GetSystemPath(FOLDER_INBOX, Path)) { auto p = LString(Path.Str()).Split("/"); for (auto f: Root->Sub.a) { auto Nm = f->GetStr(FIELD_FOLDER_NAME); if (p.Last().Equals(Nm)) { return f; } } } return NULL; } LDataPropI *LMail3Store::GetObj(int id) { switch (id) { case FIELD_INBOX: return GetSystemFolder(FOLDER_INBOX); case FIELD_OUTBOX: return GetSystemFolder(FOLDER_OUTBOX); case FIELD_SENT: return GetSystemFolder(FOLDER_SENT); case FIELD_TRASH: return GetSystemFolder(FOLDER_TRASH); } return NULL; } bool LMail3Store::OpenDb() { char f[MAX_PATH_LEN]; StatusMsg.Empty(); LMakePath(f, sizeof(f), Folder, MAIL3_DB_FILE); DbFile = f; if (!Check(sqlite3_open(f, &Db), 0)) { ErrorMsg.Printf(LLoadString(IDS_ERROR_CANT_CREATE_DB), f); return false; } else { // const char *Tbl; // for (GMail3Def *Flds=Fields.First(&Tbl); Flds; Flds=Fields.Next(&Tbl)) for (auto it : Fields) { Store3Status s = CheckTable(it.key, it.value); if (s == Store3Missing && UpdateTable(it.key, it.value, s)) { s = Store3Success; } TableStatus.Add(it.key, s); if (s == Store3Error || (s == Store3UpgradeRequired && OpenStatus != Store3Error)) OpenStatus = s; } LArray DelNames; { LStatement Tables( this, "SELECT name FROM sqlite_master " "WHERE type='table' " "ORDER BY name;"); while (Tables.Row()) { char *Name = Tables.GetStr(0); if (Name && strchr(Name, '_') && !stristr(Name, "sqlite")) DelNames.New().Reset(NewStr(Name)); } } for (unsigned i=0; i &new_items, int pos, bool is_new) { if (!Callback) return; Callback->SetContext(File, Line); Callback->OnNew(parent, new_items, pos, is_new); } bool LMail3Store::OnChange(const char *File, int Line, LArray &items, int FieldHint) { if (!Callback) return false; Callback->SetContext(File, Line); return Callback->OnChange(items, FieldHint); } bool LMail3Store::OnMove(const char *File, int Line, LDataFolderI *new_parent, LDataFolderI *old_parent, LArray &items) { if (!Callback) return false; Callback->SetContext(File, Line); return Callback->OnMove(new_parent, old_parent, items); } bool LMail3Store::OnDelete(const char *File, int Line, LDataFolderI *parent, LArray &items) { if (!Callback) return false; Callback->SetContext(File, Line); return Callback->OnDelete(parent, items); } const char *LMail3Store::GetStr(int id) { switch (id) { case FIELD_ERROR: return ErrorMsg; case FIELD_STATUS: return StatusMsg; case FIELD_TEMP_PATH: return TempPath; + case FIELD_STORE_TYPE: + return "LMail3Store"; } return NULL; } Store3Status LMail3Store::SetStr(int id, const char *s) { switch (id) { case FIELD_ERROR: ErrorMsg = s; break; case FIELD_STATUS: StatusMsg = s; break; case FIELD_TEMP_PATH: TempPath = s; break; default: LAssert(0); return Store3Error; } return Store3Success; } int64 LMail3Store::GetInt(int id) { switch (id) { case FIELD_STATUS: { if (OpenStatus != Store3Success) return OpenStatus; return IsOk() ? Store3Success : Store3Error; } case FIELD_READONLY: return false; case FIELD_VERSION: return 3; case FIELD_FORMAT: return Format; case FIELD_STORE_TYPE: return Store3Sqlite; } return -1; } Store3Status LMail3Store::SetInt(int id, int64 i) { switch (id) { case FIELD_FORMAT: break; } return Store3Error; } bool LMail3Store::Check(int Code, const char *Sql) { if (Code == SQLITE_OK || Code == SQLITE_DONE) return true; const char *Err = sqlite3_errmsg(Db); LgiTrace("%s:%i - Sqlite error %i: %s\n%s", _FL, Code, Err, Sql?Sql:(char*)"", Sql?"\n":""); LAssert(!"Db Error"); #if MAIL3_TRACK_OBJS for (int i=0; iItems.State = Store3Loaded; Root->Serialize(s, false); } } else { // Create new root folder... if ((Root = new LMail3Folder(this))) { Root->Items.State = Store3Loaded; Root->Write(MAIL3_TBL_FOLDER, true); } } } } return Root; } #define PROFILE_MOVE 0 #if PROFILE_MOVE #define PROF_MOVE(...) prof.Add(__VA_ARGS__) #else #define PROF_MOVE(...) #endif Store3Status LMail3Store::Move(LDataFolderI *NewFolder, LArray &Items) { Store3Status Status = Store3Error; LMail3Folder *To = dynamic_cast(NewFolder); if (!To) return Status; if (Items.Length() == 0) return Store3Success; #if PROFILE_MOVE LProfile prof("LMail3Store::Move"); #endif LDataFolderI *OldParent = NULL; LArray Moved; StoreTrans Tr = StartTransaction(); for (unsigned n=0; n(Items[n]); if (Fld) { PROF_MOVE("0"); if (Fld->ParentId == To->Id) Status = Store3Success; else { char s[256]; sprintf_s(s, sizeof(s), "update " MAIL3_TBL_FOLDER " set ParentId=" LPrintfInt64 " where Id=" LPrintfInt64, To->Id, Fld->Id); PROF_MOVE("1"); LStatement Stmt(this, s); if (Stmt.Exec()) { PROF_MOVE("2"); Status = Store3Success; LAssert(Fld->Parent->Sub.IndexOf(Fld) >= 0); Fld->Parent->Sub.Delete(Fld); LDataFolderI *From = Fld->Parent; Fld->Parent = To; Fld->ParentId = Fld->Parent->Id; To->Sub.Insert(Fld); if (!OldParent) OldParent = From; if (Callback && OldParent && Moved.Length() && OldParent != From) { PROF_MOVE("3"); Callback->OnMove(To, OldParent, Moved); PROF_MOVE("4"); Moved.Length(0); OldParent = From; } Moved.Add(Fld); } } } else if ((Thing = dynamic_cast(Items[n]))) { PROF_MOVE("5"); if (Thing->ParentId == To->Id) Status = Store3Success; else if (To->ItemType != MAGIC_ANY && Thing->Type() != To->ItemType) { LgiTrace("%s:%i - Can't move item (type=%x) to folder containing type %x.\n", _FL, Thing->Type(), To->ItemType); Status = Store3Error; } else { // This needs to be before the SQL update so that duplicate Mail // objects don't get created when moving to an unloaded folder. if (To->Items.GetState() != Store3Loaded) To->Children(); PROF_MOVE("6"); char s[256]; sprintf_s(s, sizeof(s), "update %s set ParentId=" LPrintfInt64 " where Id=" LPrintfInt64, Thing->GetTable(), To->Id, Thing->Id); LStatement Stmt(this, s); if (Stmt.Exec()) { PROF_MOVE("6"); Status = Store3Success; LDataFolderI *From = Thing->Parent; if (Thing->Parent) { LAssert(Thing->Parent->Items.IndexOf(Thing) >= 0); Thing->Parent->Items.Delete(Thing); } Thing->Parent = To; Thing->ParentId = To->Id; LAssert(To->Items.IndexOf(Thing) < 0); #ifdef _DEBUG if (To->System != Store3SystemTrash) { // Check there is no duplicate ID.. for (unsigned k=0; kItems.a.Length(); k++) { if (To->Items.a[k]->Id == Thing->Id) { LAssert(!"Can't have duplicate IDs in the same folder."); } } } #endif // LgiTrace("%s:%i - Saving %p:%i to %s (%i items)\n", _FL, Thing, (int)Thing->Id, To->GetStr(FIELD_FOLDER_NAME), To->Items.Length()); To->Items.Insert(Thing); if (!OldParent) OldParent = From; if (Callback && OldParent && Moved.Length() && OldParent != From) { PROF_MOVE("7"); Callback->OnMove(To, OldParent, Moved); PROF_MOVE("8"); Moved.Length(0); OldParent = From; } Moved.Add(Thing); } } } } if (Callback && Moved.Length()) Callback->OnMove(To, OldParent, Moved); return Status; } Store3Status LMail3Store::Delete(LArray &Items, bool ToTrash) { if (Items.Length() == 0) return Store3Error; LVariant FolderName; LMail3Folder *Trash = 0; if (ToTrash && Callback->GetSystemPath(FOLDER_TRASH, FolderName)) { auto t = LString(FolderName.Str()).SplitDelimit("/"); if (t.Length() == 1) Trash = Root->FindSub(t[0]); } if (Trash) { LArray MoveItems; for (unsigned i=0; i(di); if (t && t->Parent != Trash) { // Move the item to the trash instead... MoveItems.Add(di); Items.DeleteAt(i--, true); } else { LMail3Folder *f = dynamic_cast(di); if (f && f->Parent != Trash) { // Move the folder to the trash instead... MoveItems.Add(di); Items.DeleteAt(i--, true); } } } if (MoveItems.Length() > 0) { Store3Status s = Move(Trash, MoveItems); if (s != Store3Success) return s; } if (Items.Length() == 0) return Store3Success; } Store3Status s = Store3Success; LDataI *Item = Items[0]; switch ((uint32_t)Item->Type()) { default: { LMail3Thing *t = dynamic_cast(Item); if (!t) { LAssert(!"What are you trying to do?"); s = Store3Error; } else if (Callback && !Callback->OnDelete(t->Parent, Items)) { s = Store3Error; } else { for (unsigned i=0; i(Items[i]))) { if (t->DbDelete()) { if (t->Parent) t->Parent->Items.Delete(t); else LAssert(0); DeleteObj(t); } else s = Store3Error; } } } break; } case MAGIC_ATTACHMENT: { for (auto i: Items) { LMail3Attachment *a = dynamic_cast(i); if (a) { a->Delete(ToTrash); a->Detach(); delete a; } } break; } case MAGIC_FOLDER: { LMail3Folder *f = dynamic_cast(Item); if (!f) s = Store3Error; else if (Callback && !Callback->OnDelete(f->Parent, Items)) s = Store3Error; else { for (unsigned i=0; i(Items[i]))) { if (f->DbDelete()) { if (f->Parent) f->Parent->Sub.Delete(f); else LAssert(0); DeleteObj(f); } else s = Store3Error; } } } break; } } return s; } Store3Status LMail3Store::Change(LArray &Items, int PropId, LVariant &Value, LOperator Operator) { if (Items.Length() == 0) return Store3Success; if (PropId != FIELD_FLAGS) { LAssert(!"Not impl."); return Store3NotImpl; } int32 Val = Value.CastInt32(); if (!Val) { LAssert(!"One flag must be set"); return Store3Error; } if (Operator != OpPlusEquals && Operator != OpMinusEquals) { LAssert(!"Operator not supported."); return Store3Error; } StoreTrans Tr = StartTransaction(); // Set/unset flag for (unsigned i=0; i Chunk; for (unsigned n=0; n<100 && i(Items[i]); if (!m) { LAssert(!"Invalid object."); continue; } if ( (Operator == OpMinusEquals && (m->Flags & Val)) || (Operator == OpPlusEquals && !(m->Flags & Val)) ) { p.Print("%sId=" LPrintfInt64, Chunk.Length()?" or ":"", m->Id); Chunk.Add(m); } } if (Chunk.Length()) { auto Sql = p.NewGStr(); LStatement s(this, Sql); if (!s.Exec()) return Store3Error; for (unsigned n=0; n(Chunk[n]); if (m) { if (Operator == OpMinusEquals) m->Flags &= ~Val; else m->Flags |= Val; } } OnChange(_FL, Chunk, PropId); } } return Store3Success; } bool LMail3Store::SetFormat(LViewI *Parent, LDataPropI *Props) { if (!Parent || !Props) return false; Mail3SubFormat NewFormat = (Mail3SubFormat)Props->GetInt(Store3UiNewFormat); if (NewFormat == Format) return true; bool Error = false; LStatement Folders(this, "select * from " MAIL3_TBL_FOLDER); char Sql[256]; LStatement Count(this, "select Count(*) as c from " MAIL3_TBL_FOLDER); if (Count.Row()) { char *c = Count.GetStr(0); if (c) Props->SetInt(Store3UiMaxPos, atoi(c)); } if (NewFormat == Mail3v1) { // Change format to single table for all email // Create the mail table Store3Status s = CheckTable(MAIL3_TBL_MAIL, TblMail); if (!UpdateTable(MAIL3_TBL_MAIL, TblMail, s)) Error = true; else { // Copy all the sub-folder mail into that one table... while (Folders.Row()) { int64 Id = Folders.GetInt64(0); char TblName[128]; sprintf_s(TblName, sizeof(TblName), "Mail_" LPrintfInt64, Id); // Set the parent ID on all mail rows in the sub-folder table (just to be sure) sprintf_s(Sql, sizeof(Sql), "update %s set ParentId=" LPrintfInt64, TblName, Id); LStatement SetId(this, Sql); if (!SetId.Exec()) { Error = true; break; } // Now copy all the rows into the main Mail table sprintf_s(Sql, sizeof(Sql), "insert into " MAIL3_TBL_MAIL " select * from %s", TblName); LStatement Copy(this, Sql); if (!Copy.Exec()) { Error = true; break; } // And drop the sub-folder table.. sprintf_s(Sql, sizeof(Sql), "drop table if exists %s", TblName); LStatement Del(this, Sql); if (!Del.Exec()) { Error = true; break; } } } } else if (NewFormat == Mail3v2) { // Change format to a table per folder of email { LTransaction Trans(this); int Pos = 0; while (Folders.Row()) { int64 Id = Folders.GetInt64(0); char TblName[128]; sprintf_s(TblName, sizeof(TblName), "Mail_" LPrintfInt64, Id); sprintf_s(Sql, sizeof(Sql), "create table if not exists %s as select * from Mail where ParentId=" LPrintfInt64, TblName, Id); LStatement Convert(this, Sql); if (!Convert.Exec()) { Error = true; Trans.RollBack(); break; } Props->SetInt(Store3UiCurrentPos, ++Pos); } } sprintf_s(Sql, sizeof(Sql), "drop table if exists " MAIL3_TBL_FOLDER); LStatement Del(this, Sql); if (!Del.Exec()) Error = true; } if (!Error) Format = NewFormat; return !Error; } void LMail3Store::Upgrade(LViewI *Parent, LDataPropI *Props, std::function OnStatus) { bool Status = true; LStringPipe p; for (auto it : Fields) { Store3Status s = TableStatus.Find(it.key); if (s == Store3UpgradeRequired) { if (!UpdateTable(it.key, it.value, s)) { p.Print("Failed to upgrade %s\n", it.key); Status = false; } } } if (Props && !Status) { LAutoString a(p.NewStr()); Props->SetStr(Store3UiError, a); } if (OnStatus) OnStatus(Status); } class SqliteRepairThread : public LThread { LMail3Store *Store; LString Exe; LString Db; LString RepairSql; LString OldDb; LViewI *Parent = NULL; LDataPropI *Props = NULL; LAutoPtr Shell; ssize_t Ch = 0; char Line[512] = {}; public: bool Status = false; SqliteRepairThread(LMail3Store *store, LString exe, LString db, LViewI *parent, LDataPropI *props) : Store(store), LThread("SqliteRepairThread") { Exe = exe; Db = db; Parent = parent; Props = props; char p[MAX_PATH_LEN]; LMakePath(p, sizeof(p), Db, "..\\Repair.sql"); for (char *c = p; *c; c++) { if (*c == '\\') *c = '/'; } RepairSql = p; LString s; LDateTime dt; dt.SetNow(); char sNow[64]; dt.Get(sNow, sizeof(sNow)); for (char *c = sNow; *c; c++) { if (*c == ':' || *c == '/') *c = '-'; } s.Printf("..\\Database %s.sqlite", sNow); LMakePath(p, sizeof(p), Db, s); OldDb = p; LgiTrace("Sqlite Repair paths:\n" "Exe: '%s'\n" "Db: '%s'\n" "Old: '%s'\n" "Sql: '%s'\n", Exe.Get(), Db.Get(), OldDb.Get(), RepairSql.Get()); Run(); } char *Read() { Ch = Shell->Read(Line, sizeof(Line)-1); if (Ch < 0) return NULL; Line[Ch] = 0; return Line; } bool GetPrompt() { char *l; while ((l = Read())) { if (stristr(l, "sqlite>")) return true; } if (Props) Props->SetStr(Store3UiError, "Failed to get prompt..."); return false; } void StatusMsg(const char *msg) { if (Props) { int64 p = Props->GetInt(Store3UiCurrentPos); Props->SetInt(Store3UiCurrentPos, p + 1); Props->SetStr(Store3UiStatus, msg); } } int OnStatus(int s) { // Send status event back to the store... auto msg = new LMail3StoreMsg(LMail3StoreMsg::MsgRepairComplete); msg->Int = s; Store->PostStore(msg); return s; } int Main() { if (Props) { Props->SetInt(Store3UiMaxPos, 7); Props->SetStr(Store3UiStatus, "Starting sqlite..."); } // Open the shell and start the export process LString Args; Args.Printf("-interactive %s", Db.Get()); if (!Shell.Reset(new LSubProcess(Exe, Args))) return OnStatus(-1); if (!Shell->Start(true, true)) { if (Props) Props->SetStr(Store3UiError, "Couldn't execute sqlite3 shell."); return OnStatus(-2); } // http://froebe.net/blog/2015/05/27/error-sqlite-database-is-malformed-solved/ // // Run through the commands: // pragma integrity_check; // .mode insert // .output mydb_export.sql // .dump // .exit if (!GetPrompt()) return OnStatus(-3); StatusMsg("Setting mode..."); LString Cmd; Cmd.Printf(".mode insert\r\n"); Shell->Write(Cmd, Cmd.Length()); if (!GetPrompt()) return OnStatus(-4); StatusMsg("Setting output file..."); Cmd.Printf(".output \"%s\"\r\n", RepairSql.Get()); Shell->Write(Cmd, Cmd.Length()); if (!GetPrompt()) return OnStatus(-5); StatusMsg("Dumping SQL..."); Cmd.Printf(".dump\r\n"); Shell->Write(Cmd, Cmd.Length()); if (!GetPrompt()) return OnStatus(-6); Cmd.Printf(".exit\r\n"); Shell->Write(Cmd, Cmd.Length()); Shell->Wait(); StatusMsg("Renaming database..."); if (!FileDev->Move(Db, OldDb)) { LString s; s.Printf("Can't move '%s' to '%s'", Db.Get(), OldDb.Get()); Props->SetStr(Store3UiError, s); return -6; } StatusMsg("Importing SQL..."); char *r = RepairSql; for (char *c = r; *c; c++) { if (*c == '/' || *c == '\\') *c = DIR_CHAR; } #ifdef WINDOWS wchar_t wd[MAX_PATH_LEN]; _wgetcwd(wd, MAX_PATH_LEN); #else char wd[MAX_PATH_LEN]; #ifdef __GTK_H__ // Gtk:: #endif getcwd(wd, MAX_PATH_LEN); #endif char base[MAX_PATH_LEN]; LMakePath(base, sizeof(base), Db, ".."); #ifdef WINDOWS LAutoWString baseW(Utf8ToWide(base)); _wchdir(baseW); #else #ifdef __GTK_H__ // Gtk:: #endif chdir(base); #endif char *exe_leaf = strrchr(Exe, DIR_CHAR); Args.Printf("%s \"%s\" < \"%s\"", exe_leaf ? exe_leaf + 1 : Exe.Get(), Db.Get(), RepairSql.Get()); int Result = system(Args); LgiTrace("Import result = %i\n", Result); #ifdef WINDOWS _wchdir(wd); #else #ifdef __GTK_H__ // Gtk:: #endif chdir(wd); #endif StatusMsg("Deleting temporary files..."); FileDev->Delete(RepairSql, false); return OnStatus(0); } }; bool CheckPathForFile(const char *File, char *Exe, int ExeSize) { LString Path; #ifdef WINDOWS char *buffer = NULL; size_t sz = 0; errno_t err = _dupenv_s(&buffer, &sz, "PATH"); if (err) { LgiTrace("%s:%i - _dupenv_s failed with %i.\n", _FL, err); return false; } if (sz == 0) // PATH not found return false; Path = buffer; free(buffer); #else Path = getenv("PATH"); if (!Path) return false; #endif LString::Array p = Path.Split(LGI_PATH_SEPARATOR); for (unsigned i=0; i OnStatus) { // Is the sqlite3 shell binary available? char base[MAX_PATH_LEN]; char exe[MAX_PATH_LEN] = ""; LMakePath(base, sizeof(base), DbFile, ".."); #ifdef WINDOWS const char *SqliteBin = "sqlite3.exe"; LMakePath(exe, sizeof(exe), base, SqliteBin); #else const char *SqliteBin = "sqlite3"; CheckPathForFile(SqliteBin, exe, sizeof(exe)); #endif if (!LFileExists(exe)) { LString DownloadUrl = "https://www.sqlite.org/download.html"; LViewI *p = dynamic_cast(Props); LString Msg; Msg.Printf("Error: sqlite shell binary missing (%s). " #ifdef WINDOWS "Download from:\n" "\n" " %s\n" #else "Install using your package manager:\n" "\n" " e.g. sudo apt-get install sqlite3\n" #endif "\n" "Download the shell binary and extract to this folder:\n" "\n" " %s\n" "\n" "Then try the Repair command again.\n", SqliteBin, DownloadUrl.Get(), base); auto Dlg = new LAlert(p?p:Parent, "LMail3Store::Repair", Msg, "Browse Download Site & Local Folder", "Cancel"); Dlg->DoModal([this, Dlg, DownloadUrl, base](auto dlg, auto ctrlId) { if (ctrlId == 1) { LExecute(DownloadUrl); LExecute(base); } delete dlg; }); if (OnStatus) OnStatus(true); return; } // Close our database... CloseDb(); // Do the repair in a thread RepairOnStatus = OnStatus; new SqliteRepairThread(this, exe, DbFile, Parent, Props); } int64 LMail3Store::GetFolderId(char *Path) { auto Parts = LString(Path).SplitDelimit("/"); LMail3Folder *Root = dynamic_cast(GetRoot()); if (!Root) return 0; uint64 Id = Root->Id; for (unsigned i=0; iSetStr(Store3UiError, s); } void SetStatus(const char *s) { Props->SetStr(Store3UiStatus, s); } void OnStatus(bool s) { // Send status event back to the store... auto msg = new LMail3StoreMsg(LMail3StoreMsg::MsgCompactComplete); msg->Int = s; Store->PostStore(msg); } int Main() { // Clean up orphaned mail segments. { SetStatus("Cleaning up orphaned Mail segments..."); LMail3Store::LStatement s(Store, "delete from " MAIL3_TBL_MAILSEGS " where MailId not in (select Id from " MAIL3_TBL_MAIL ")"); if (!s.Exec()) { SetError("Failed to delete orphaned segments."); OnStatus(false); return -1; } } // Clean up orphaned mail. if (!IsCancelled()) { SetStatus("Cleaning up orphaned Mail..."); LMail3Store::LStatement count(Store, "select COUNT(*) from " MAIL3_TBL_MAIL " where ParentId not in (select Id from " MAIL3_TBL_FOLDER ")"); if (count.Row()) { int64 Rows = count.GetInt64(0); Max = Rows; } LDataStoreI::StoreTrans Trn = Store->StartTransaction(); LMail3Store::LStatement s(Store, "select Id from " MAIL3_TBL_MAIL " where ParentId not in (select Id from " MAIL3_TBL_FOLDER ")"); int64 RowPos = 0; while (!IsCancelled() && s.Row()) { int64 Id = s.GetInt64(0); Store->DeleteMailById(Id); Value = ++RowPos; } Max = 1; Value = 0; } // Vacuum if (!IsCancelled()) { SetStatus("Vacuum unused space..."); LMail3Store::LStatement s(Store, "vacuum;"); if (!s.Exec()) { SetError("Reclaiming space failed."); OnStatus(false); return -1; } } OnStatus(true); return 0; } }; void LMail3Store::Compact(LViewI *Parent, LDataPropI *Props, std::function OnStatus) { if (!Props || !Callback) return; bool Status = false; LVariant InboxPath; if (!Callback->GetSystemPath(FOLDER_INBOX, InboxPath)) Props->SetStr(Store3UiError, "Couldn't get path to Inbox."); else { int64 InboxId = GetFolderId(InboxPath.Str()); if (!InboxId) Props->SetStr(Store3UiError, "Couldn't get Inbox ID."); { // Save this locally, because the thread isn't suitable for calling it. // Instead it will send us a message, and then the store can call it from // the GUI thread. This way any client implementing the callback can talk // to or delete UI elements. CompactOnStatus = OnStatus; new CompactThread(this, InboxId, Props); return; // without calling OnStatus, the CompactThread will do it. } } OnStatus(Status); } void LMail3Store::OnEvent(void *Param) { auto msg = (LMail3StoreMsg*)Param; switch (msg->Msg) { case LMail3StoreMsg::MsgCompactComplete: { if (CompactOnStatus) { CompactOnStatus(msg->Int); CompactOnStatus = NULL; } else LgiTrace("%s:%i - No CompactOnStatus to call on MsgCompactComplete.\n", _FL); break; } case LMail3StoreMsg::MsgRepairComplete: { OpenDb(); if (RepairOnStatus) { RepairOnStatus(msg->Int >= 0); RepairOnStatus = NULL; } break; } default: { LgiTrace("%s:%i - Unhandled mail3store event.\n", _FL); break; } } } bool LMail3Store::IsOk() { return #ifndef __llvm__ this != 0 && #endif Db != 0; } bool LMail3Store::ParseTableFormat(const char *Name, TableDefn &Defs) { char Sql[256]; sprintf_s(Sql, sizeof(Sql), "SELECT * FROM sqlite_master WHERE type='table' and name='%s'", Name); LStatement st(this, Sql); if (!st.IsOk()) return false; if (!st.Row()) return false; char *Fmt = st.GetStr(4); char *s = Fmt ? strchr(Fmt, '(') + 1 : 0; char *e = Fmt ? strrchr(Fmt, ')') : 0; if (!s || !e) return false; LString f(s, e - s); Defs.t = f.SplitDelimit(","); for (unsigned i=0; i= Defs.Length()) { LString Msg; Msg.Printf("\nTable '%s' is missing the schema field: '%s %s'", Name, Flds[i].Type, Flds[i].Name); StatusMsg += Msg; break; } GMail3Def &Def = Defs[i]; if (i >= FieldCount) // Schema field count is less than existing table { LString Msg; Msg.Printf("\nTable '%s' has an extra field: '%s %s'\n", Name, Def.Type, Def.Name); StatusMsg += Msg; break; } if (!Flds[i].Name || !Flds[i].Type || !Def.Name || !Def.Type) { Status = Store3Error; break; } if (_stricmp(Flds[i].Name, Def.Name) != 0 || _stricmp(Flds[i].Type, Def.Type) != 0) { LString Msg; Msg.Printf("\nTable '%s' has a field '%s %s' which is to the schema: '%s %s'\n", Name, Def.Type, Def.Name, Flds[i].Type, Flds[i].Name); StatusMsg += Msg; Status = Store3UpgradeRequired; break; } } if (Defs.Length() != FieldCount) { LString Msg; Msg.Printf("\nTable '%s' has %i fields (should have %i)\n", Name, Defs.Length(), FieldCount); StatusMsg += Msg; Status = Store3UpgradeRequired; } } else Status = Store3Missing; return Status; } bool LMail3Store::UpdateTable(const char *Name, GMail3Def *Flds, Store3Status Check) { LStatement st(this); LAutoString TempTable; TableDefn Defs; if (Check == Store3UpgradeRequired) { // Get the old table format so we can copy over field by field later... if (!ParseTableFormat(Name, Defs)) { return false; } char Tmp[256]; sprintf_s(Tmp, sizeof(Tmp), "%s_tmp", Name); TempTable.Reset(NewStr(Tmp)); char AlterSql[256]; sprintf_s(AlterSql, sizeof(AlterSql), "alter table %s rename to %s", Name, Tmp); LStatement Alter(this, AlterSql); if (!Alter.Exec()) { LAssert(!"Can't rename table."); return false; } } // Create table? LStringPipe p; LHashTbl, bool> AllFlds; p.Print("create table %s (", Name); for (int i=0; Flds[i].Name; i++) { AllFlds.Add(Flds[i].Name, true); if (i) p.Print(", "); p.Print("%s %s", Flds[i].Name, Flds[i].Type); } p.Print(")"); LAutoString s(p.NewStr()); if (s) { if (st.Prepare(s)) st.Exec(); else { LgiTrace("Sql='%s'\n", s.Get()); return false; } } // Copy over data from the old table if (TempTable) { LStringPipe p; p.Print("insert into %s (", Name); int Count = 0; for (unsigned i=0; iGetFields(Tbl); if (f) { LVariant v; LStringPipe p; p.Print("insert into '%s' values (", Tbl); for (int i=0; f[i].Name; i++) { if (i) p.Print(","); p.Print("?"); } p.Print(")"); v.OwnStr(p.NewStr()); Prepare(v.Str()); } } LMail3Store::LUpdate::LUpdate(LMail3Store *store, const char *Tbl, int64 rowid, char *ExcludeField) : LStatement(store) { RowId = rowid; Store = store; Table = Tbl; LAssert(Store != 0 && Tbl != 0 && RowId > 0); GMail3Def *f = Store->GetFields(Tbl); if (f) { LVariant v; LStringPipe p; p.Print("update %s set ", Tbl); for (int i=1; f[i].Name; i++) { if (ExcludeField && !_stricmp(f[i].Name, ExcludeField)) continue; if (i>1) p.Print(", "); p.Print("%s=?%i", f[i].Name, i + 1); } p.Print(" where %s=?1", f[0].Name); v.OwnStr(p.NewStr()); Prepare(v.Str()); } } //////////////////////////////////////////////////////////////////////// bool LMail3Obj::Check(int r, char *sql) { return Store->Check(r, sql); } #define DEBUG_MAIL3_WRITE 0 bool LMail3Obj::Write(const char *Table, bool Insert) { #if DEBUG_MAIL3_WRITE LProfile Prof("LMail3Obj::Write"); #endif LAutoPtr s; #if DEBUG_MAIL3_WRITE Prof.Add(_FL); #endif if (Insert) s.Reset(new LMail3Store::LInsert(Store, Table)); else s.Reset(new LMail3Store::LUpdate(Store, Table, Id)); #if DEBUG_MAIL3_WRITE Prof.Add(_FL); #endif if (s) { Serialize(*s, true); #if DEBUG_MAIL3_WRITE Prof.Add(_FL); #endif if (s->Exec()) { #if DEBUG_MAIL3_WRITE Prof.Add(_FL); #endif if (Insert) { Id = s->LastInsertId(); if (Id < 0) { LAssert(!"No ID returned."); LgiTrace("%s:%i - No ID from statement.\n", _FL); } } } else { LgiTrace("%s:%i - Exec failed.\n", _FL); } } else LAssert(!"No statement"); return Id >= 0; } ////////////////////////////////////////////////////////////////////////// LMail3Store::LStatement::LStatement(LMail3Store *store, const char *sql) { Store = store; s = 0; #if MAIL3_TRACK_OBJS LMail3Store::SqliteObjs &_d = Store->All.New(); _d.Stat = this; #endif if (sql) Prepare(sql); } LMail3Store::LStatement::~LStatement() { Finalize(); #if MAIL3_TRACK_OBJS Store->RemoveFromAll(this); #endif } int64 LMail3Store::LStatement::LastInsertId() { return sqlite3_last_insert_rowid(Store->GetDb()); } bool LMail3Store::LStatement::Prepare(const char *Sql) { Finalize(); TempSql.Reset(NewStr(Sql)); Store->Check(sqlite3_prepare_v2(Store->GetDb(), Sql, -1, &s, 0), Sql); return s != 0; } bool LMail3Store::LStatement::Finalize() { if (!s) return false; bool Status = Store->Check(sqlite3_finalize(s), 0); s = 0; return Status; } bool LMail3Store::LStatement::Row() { if (!IsOk()) return false; int r = sqlite3_step(s); return r == SQLITE_ROW; } bool LMail3Store::LStatement::Exec() { if (!IsOk()) return false; if (!Store->Check(sqlite3_step(s), TempSql)) return false; bool Status = true; if (Post.Length()) { int64 Id = GetRowId(); LAssert(Id > 0); for (unsigned i=0; iCheck(sqlite3_blob_open( Store->GetDb(), 0, Table.Str(), b.ColName.Str(), Id, true, &Blob), 0)) { LArray Buf; if (Buf.Length(32<<10)) { ssize_t r, Pos = 0; b.Data->SetPos(0); while ((r = b.Data->Read(&Buf[0], Buf.Length())) > 0) { if (!Store->Check(sqlite3_blob_write(Blob, &Buf[0], (int)r, (int)Pos), 0)) { Status = false; break; } Pos += r; } } else Status = false; Store->Check(sqlite3_blob_close(Blob), 0); } } Post.Length(0); } return Status; } bool LMail3Store::LStatement::Reset() { if (!IsOk()) return false; return Store->Check(sqlite3_reset(s), 0) && Store->Check(sqlite3_clear_bindings(s), 0); } /* All the getter functions use 'Col' as the first column is '0' */ int LMail3Store::LStatement::GetSize(int Col) { return IsOk() ? sqlite3_column_bytes(s, Col) : 0; } bool LMail3Store::LStatement::GetBool(int Col) { if (!IsOk() || sqlite3_column_type(s, Col) == SQLITE_NULL) return false; return sqlite3_column_int(s, Col) != 0; } int LMail3Store::LStatement::GetInt(int Col) { if (!IsOk() || sqlite3_column_type(s, Col) == SQLITE_NULL) return -1; return sqlite3_column_int(s, Col); } int64 LMail3Store::LStatement::GetInt64(int Col) { if (!IsOk() || sqlite3_column_type(s, Col) == SQLITE_NULL) return -1; return sqlite3_column_int64(s, Col); } bool LMail3Store::LStatement::SetInt64(int Col, int64 n) { if (!IsOk()) return false; if (n == -1) return Store->Check(sqlite3_bind_null(s, Col+1), 0); else return Store->Check(sqlite3_bind_int64(s, Col+1, n), 0); } char *LMail3Store::LStatement::GetStr(int Col) { return IsOk() ? (char*)sqlite3_column_text(s, Col) : 0; } bool LMail3Store::LStatement::GetBinary(int Col, LVariant *v) { if (!v) return false; const void *Ptr = sqlite3_column_blob(s, Col); if (!Ptr) return false; int Bytes = sqlite3_column_bytes(s, Col); return v->SetBinary(Bytes, (void*)Ptr); } /* All the setter functions use 'Col+1' as the first column is '1' */ bool LMail3Store::LStatement::SetInt(int Col, int n) { if (!IsOk()) return false; if (n == -1) return Store->Check(sqlite3_bind_null(s, Col+1), 0); else return Store->Check(sqlite3_bind_int(s, Col+1, n), 0); } bool LMail3Store::LStatement::SetStr(int Col, char *Str) { if (!IsOk()) return false; if (Str) return Store->Check(sqlite3_bind_text(s, Col+1, Str, -1, SQLITE_STATIC), 0); else return Store->Check(sqlite3_bind_null(s, Col+1), 0); } bool LMail3Store::LStatement::SetDate(int Col, LDateTime &Var) { if (!Var.Year()) return Store->Check(sqlite3_bind_null(s, Col+1), 0); char c[64]; sprintf_s(c, sizeof(c), "%4.4i-%2.2i-%2.2i %2.2i:%2.2i:%2.2i", Var.Year(), Var.Month(), Var.Day(), Var.Hours(), Var.Minutes(), Var.Seconds()); return Store->Check(sqlite3_bind_text(s, Col+1, (const char*)c, -1, SQLITE_TRANSIENT), 0); }; bool LMail3Store::LStatement::SetStream(int Col, const char *ColName, LStreamI *Data) { if (!IsOk() || !Data) return false; PostBlob &p = Post.New(); p.ColName = ColName; p.Data = Data; p.Size = Data->GetSize(); LAssert((p.Size & 0xffffffff00000000) == 0); return Store->Check(sqlite3_bind_zeroblob(s, Col+1, (int)p.Size), 0); } bool LMail3Store::LStatement::SetBinary(int Col, const char *ColName, LVariant *v) { if (!v || v->Type != GV_BINARY) { LAssert(!"Invalid binary type."); return false; } if (!Store->Check(sqlite3_bind_blob(s, Col+1, v->Value.Binary.Data, (int)v->Value.Binary.Length, SQLITE_TRANSIENT), NULL)) return false; return true; } ////////////////////////////////////////////////////////////////////////// LMail3Store::LTransaction::LTransaction(LMail3Store *store) { Store = store; const char *Sql = "begin;"; Open = Store ? Store->Check(sqlite3_exec(Store->GetDb(), Sql, 0, 0, 0), Sql) : false; } LMail3Store::LTransaction::~LTransaction() { if (Open) { const char *Sql = "end;"; Store->Check(sqlite3_exec(Store->GetDb(), Sql, 0, 0, 0), Sql); } } bool LMail3Store::LTransaction::RollBack() { bool Status = false; if (Open) { const char *Sql = "rollback;"; Status = Store->Check(sqlite3_exec(Store->GetDb(), Sql, 0, 0, 0), Sql); Open = false; } return Status; } ///////////////////////////////////////////////////////////////////////// LDataStoreI *OpenMail3(const char *Mail3Folder, LDataEventsI *Callback, bool Create) { return new LMail3Store(Mail3Folder, Callback, Create); } diff --git a/Code/Store3Mapi/ScribeMapi_Store.cpp b/Code/Store3Mapi/ScribeMapi_Store.cpp --- a/Code/Store3Mapi/ScribeMapi_Store.cpp +++ b/Code/Store3Mapi/ScribeMapi_Store.cpp @@ -1,838 +1,840 @@ #define INITGUID #define USES_IID_IMAPIAdviseSink #include #if _MSC_VER < _MSC_VER_VS2013 #include #endif #include "ScribeMapi.h" ///////////////////////////////////////////////////////////////////////////// class LMapiAdviseSink : public IMAPIAdviseSink { LMapiStore *Store; volatile LONG Refs; public: #if _MSC_VER < _MSC_VER_VS2013 ULONG Connection; #else ULONG_PTR Connection; #endif LMapiAdviseSink(LMapiStore *store) { Store = store; Refs = 1; Connection = 0; } ~LMapiAdviseSink() { LAssert(Refs == 0); } HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, LPVOID FAR * ppvObj) { if (!ppvObj) return E_INVALIDARG; *ppvObj = NULL; if (IsEqualGUID(IID_IMAPIAdviseSink, riid)) { *ppvObj = this; AddRef(); return NOERROR; } return E_NOINTERFACE; } ULONG STDMETHODCALLTYPE AddRef() { return InterlockedIncrement(&Refs); } ULONG STDMETHODCALLTYPE Release() { if (Refs == 0) return 0; LONG r = InterlockedDecrement(&Refs); if (r == 0) delete this; return r; } ULONG STDMETHODCALLTYPE OnNotify(ULONG cNotif, LPNOTIFICATION lpNotifications) { if (!lpNotifications) return E_POINTER; return Store->OnNotify(cNotif, lpNotifications); } }; ///////////////////////////////////////////////////////////////////////////// LMapiStore::LMapiStore(const char *profile, const char *username, const char *password, uint64 accountId, LDataEventsI *callback) { Session = NULL; MsgStore = NULL; Profile = profile; Username = username; Password = password; Callback = callback; AccountId = accountId; Notify = NULL; MapiInitialized = false; Root = NULL; LViewI *v = dynamic_cast(Callback); Ui = #ifndef __GTK_H__ v ? (UI_TYPE)v->Handle() : #endif NULL; if (!Load("mapi32.dll")) { Error("%s:%i - Failed to load \"mapi32.dll\".\n", _FL); } MAPIInitialize = (MAPIINITIALIZE*) GetAddress("MAPIInitialize"); MAPILogonEx = (MAPILOGONEX*) GetAddress("MAPILogonEx"); MAPIAllocateBuffer = (MAPIALLOCATEBUFFER*) GetAddress("MAPIAllocateBuffer"); MAPIFreeBuffer = (MAPIFREEBUFFER*) GetAddress("MAPIFreeBuffer"); WrapCompressedRTFStream = (pWrapCompressedRTFStream) GetAddress("WrapCompressedRTFStream"); MAPIUninitialize = (MAPIUNINITIALIZE*) GetAddress("MAPIUninitialize"); if (Login()) { ScribeMsgStores Stores(this, Session); for (unsigned i=0; iDisplayName && stristr(e->DisplayName, Username)) { HRESULT res = Session->OpenMsgStore(Ui, (ULONG)e->Entry.Length(), // entry bytes (LPENTRYID)&e->Entry[0],// ptr to entry NULL, // default interface: IMsgStore MAPI_BEST_ACCESS, &MsgStore); if (SUCCEEDED(res)) { Stores.Delete(e); EntryRef.Reset(e); break; } } } if (MsgStore) { ULONG size = 0; LPENTRYID entry = NULL; HRESULT res = MsgStore->GetReceiveFolder ( NULL, // Get default receive folder MAPI_UNICODE, // Flags &size, // Size and ... &entry, // Value of the EntryID to be returned NULL // You don't care to see the class returned ); if (SUCCEEDED(res)) { InboxEntry.Add((uint8_t*)entry, size); MAPIFreeBuffer(entry); #if 1 // Setup notification Notify = new LMapiAdviseSink(this); if (Notify) { res = MsgStore->Advise( (ULONG)InboxEntry.Length(), (LPENTRYID)&InboxEntry[0], fnevNewMail | fnevObjectCreated | fnevObjectDeleted | fnevObjectModified | fnevObjectMoved, Notify, &Notify->Connection); } #endif } else Error("%s:%i - GetReceiveFolder failed (0x%x).\n", _FL, res); } } } LMapiStore::~LMapiStore() { MAPIUninitialize = NULL; MAPIInitialize = NULL; MAPILogonEx = NULL; MAPIAllocateBuffer = NULL; MAPIFreeBuffer = NULL; WrapCompressedRTFStream = NULL; // Make sure we have cleaned up SetInt(FIELD_IS_ONLINE, false); if (MapiInitialized && MAPIUninitialize) MAPIUninitialize(); } ULONG LMapiStore::OnNotify(ULONG cNotif, LPNOTIFICATION lpNotifications) { LMapiFolder *Inbox = FindSystemFolder(Store3SystemInbox); if (!Inbox) return S_OK; LArray NewItems, DelItems; for (ULONG i=0; iulEventType) { case fnevNewMail: { LAutoPtr nm(new LMapiMail(this)); if (nm) { SPropValue e; ZeroObj(e); e.Value.bin.lpb = (LPBYTE)n->info.newmail.lpEntryID; e.Value.bin.cb = n->info.newmail.cbEntryID; nm->Set(&e, Inbox, NULL); NewItems.Add(nm); Inbox->Items.Insert(nm.Release()); } break; } case fnevObjectDeleted: { OBJECT_NOTIFICATION *o = &n->info.obj; int asd=0; break; } case fnevObjectCreated: { OBJECT_NOTIFICATION *o = &n->info.obj; int asd=0; break; } case fnevObjectModified: { OBJECT_NOTIFICATION *o = &n->info.obj; int asd=0; break; } case fnevObjectMoved: { OBJECT_NOTIFICATION *o = &n->info.obj; int asd=0; break; } } } if (NewItems.Length() && Callback) Callback->OnNew(Inbox, NewItems, -1, true); if (DelItems.Length() && Callback) Callback->OnDelete(Inbox, DelItems); return S_OK; } bool LMapiStore::Error(const char *Fmt, ...) { va_list arg; va_start(arg, Fmt); char buffer[256]; int ch = vsprintf_s(buffer, sizeof(buffer), Fmt, arg); va_end(arg); if (ch > 0) LgiTrace("%s", buffer); return false; } bool LMapiStore::Login() { char16 Cur[MAX_PATH_LEN]; GetCurrentDirectory(CountOf(Cur), Cur); if (!IsLoaded() || !MAPIInitialize || !MAPILogonEx || !MAPIAllocateBuffer || !MAPIFreeBuffer) { Error("%s:%i - Missing address of MAPI functions.\n", _FL); return false; } HRESULT res = MAPIInitialize(NULL); if (FAILED(res)) { Error("%s:%i - MAPIInitialize failed with 0x%x.\n", _FL, res); return false; } LAutoWString wProfile(Utf8ToWide(Profile)); LAutoWString wPassword(Utf8ToWide(Password)); MapiInitialized = true; res = MAPILogonEx( Ui, wProfile, wPassword, MAPI_LOGON_UI | MAPI_EXTENDED, &Session); if (FAILED(res) || !Session) { if (MAPIUninitialize) MAPIUninitialize(); Error("%s:%i - MAPILogonEx failed (0x%x).\n", _FL, res); return false; } SetCurrentDirectory(Cur); return true; } LMapiFolder *LMapiStore::FindSystemFolder(Store3SystemFolder Type) { LDataFolderI *r = GetRoot(); if (!r) return NULL; LDataIterator &it = r->SubFolders(); for (LDataFolderI *f=it.First(); f; f=it.Next()) { if (f->GetInt(FIELD_SYSTEM_FOLDER) == Type) { LMapiFolder *Trash = dynamic_cast(f); LAssert(Trash != NULL); return Trash; } } return NULL; } Store3Status LMapiStore::SetInt(int id, int64 i) { switch (id) { case FIELD_IS_ONLINE: { if (!i) { if (Root) Root->ReleaseHandle(); EntryRef.Reset(); if (Notify) { ULONG r = Notify->Release(); Notify = NULL; } if (MsgStore) { ULONG r = MsgStore->Release(); MsgStore = NULL; } if (Session) { HRESULT res = Session->Logoff(Ui, 0, 0); if (FAILED(res)) { Error("%s:%i - Session->Logoff failed with %x\n", _FL, res); } ULONG r = Session->Release(); LAssert(r == 0); Session = NULL; } } break; } default: LAssert(0); return Store3Error; } return Store3Success; } int64 LMapiStore::GetInt(int id) { switch (id) { case FIELD_IS_ONLINE: return Session != NULL; break; case FIELD_ACCOUNT_ID: return AccountId; default: LAssert(0); break; } return -1; } const char *LMapiStore::GetStr(int id) { switch (id) { case FIELD_FOLDER_NAME: RootName = Username + "@" + Profile; return RootName; + case FIELD_STORE_TYPE: + return "LMapiStore"; } return NULL; } bool LMapiStore::OnChange(const char *File, int Line, LArray &items, int FieldHint) { if (!Callback) return false; Callback->SetContext(File, Line); return Callback->OnChange(items, FieldHint); } LDataI *LMapiStore::Create(int Type) { switch (Type) { case MAGIC_MAIL: return new LMapiMail(this); case MAGIC_FOLDER: return new LMapiFolder(this); case MAGIC_CALENDAR: return new LMapiCalendar(this); case MAGIC_CONTACT: return new LMapiContact(this); default: LAssert(!"Unsupport item type."); Error("%s:%i - Unsupported create type 0x%x\n", _FL, Type); break; } return NULL; } LDataFolderI *LMapiStore::GetRoot(bool create) { if (Session && !Root) { if (EntryRef) { IMAPIFolder *r = NULL; if (EntryRef->OpenRoot(Session, Ui, &MsgStore, &r)) { if ((Root = new LMapiFolder(this))) { Root->SetStr(FIELD_FOLDER_NAME, EntryRef->DisplayName); Root->Set(r); } } } // else LAssert(0); } return Root; } Store3Status LMapiStore::Move(LDataFolderI *NewFolder, LArray &Items) { LMapiFolder *Dest = dynamic_cast(NewFolder); if (!Dest || Items.Length() == 0) { return Store3Error; } LArray Moved; LMapiFolder *Source = NULL; LArray Entries; for (unsigned i=0; i(Items[i]); if (t && t->Parent) { if (!Source) Source = t->Parent; else if (Source != t->Parent) { Items.DeleteAt(i--); continue; } SBinary &bin = Entries.New(); bin.lpb = &t->Entry[0]; bin.cb = (ULONG)t->Entry.Length(); if (t->UserData) Moved.Add(t); } else Items.DeleteAt(i--); } ENTRYLIST Msgs; Msgs.cValues = (ULONG)Entries.Length(); Msgs.lpbin = &Entries[0]; if (!Items.Length()) { LAssert(!"No valid items to move."); return Store3Error; } if (!Source->Handle() || !Dest->Handle()) { LAssert(!"Can't get folder handle."); return Store3Error; } HRESULT res = Source->Handle()->CopyMessages ( &Msgs, NULL, Dest->Handle(), Ui, NULL, // Progress MESSAGE_MOVE // Flags ); if (FAILED(res)) { LAssert(!"CopyMessages failed."); Error("%s:%i - CopyMessages failed with 0x%x\n", _FL, res); return Store3Error; } for (unsigned i=0; i(Moved[i]); if (t) { Source->Items.a.Delete(t); Dest->Items.a.Add(t); t->Parent = Dest; } } if (Moved.Length() > 0 && Callback && !Callback->OnMove(NewFolder, Source, Moved)) { return Store3Error; } return Store3Success; } Store3Status LMapiStore::Delete(LArray &Items, bool ToTrash) { if (Items.Length() == 0) return Store3Error; LMapiFolder *Trash = ToTrash ? FindSystemFolder(Store3SystemTrash) : NULL; if (Trash) { LArray MoveItems; for (unsigned i=0; i(di); if (t && t->Parent != Trash) { // Move the item to the trash instead... MoveItems.Add(di); Items.DeleteAt(i--, true); } } if (MoveItems.Length() > 0) { Store3Status s = Move(Trash, MoveItems); if (s != Store3Success) return s; } if (Items.Length() == 0) return Store3Success; } Store3Status s = Store3Success; LDataI *Item = Items[0]; switch (Item->Type()) { default: { LMapiThing *t = dynamic_cast(Item); if (!t) s = Store3Error; else if (Callback && !Callback->OnDelete(t->Parent, Items)) s = Store3Error; else { LArray Del; LMapiFolder *Parent = NULL; for (unsigned i=0; i(Items[i]); if (!t) continue; if (!Parent) Parent = t->Parent; if (t->Parent != NULL && t->Parent == Parent) { Del.Add(t); } } if (Del.Length() > 0) { ENTRYLIST Msgs; LArray Entries; Msgs.cValues = (ULONG)Del.Length(); Entries.Length(Del.Length()); for (unsigned i=0; iEntry.Length(); e.lpb = &t->Entry[0]; } Msgs.lpbin = &Entries[0]; HRESULT Status = Parent->Handle()->DeleteMessages( &Msgs, NULL/*UI*/, NULL/*Prog*/, 0/*Flags*/); if (SUCCEEDED(Status)) { for (unsigned i=0; iParent->Items.Delete(t); t->Parent = NULL; DeleteObj(t); } } else { Error("%s:%i - DeleteMessages failed with 0x%x\n", _FL, Status); s = Store3Error; } } } break; } case MAGIC_FOLDER: { for (unsigned i=0; i(Items[i]); if (!f) { s = Store3Error; break; } LArray Del; Del.Add(f); if (Callback && !Callback->OnDelete(f->Parent, Del)) { s = Store3Error; break; } if (f->Parent && f->Parent->MapiFolder) { HRESULT res = f->Parent->MapiFolder->DeleteFolder ( (ULONG)f->Entry.Length(), (LPENTRYID) &f->Entry[0], Ui, NULL, // Progress 0 // Flags ); if (FAILED(res)) { Error("%s:%i - Failed to delete folder '%s', err=0x%x\n", _FL, f->Name.Get(), res); s = Store3Error; break; } } if (f->Parent) f->Parent->Sub.Delete(f); else LAssert(0); DeleteObj(f); } break; } } return s; } Store3Status LMapiStore::Change(LArray &Items, int PropId, LVariant &Value, LOperator Operator) { switch (PropId) { case FIELD_FLAGS: { uint64 v = Value.CastInt64(); if (v == MAIL_READ) { LArray Changed; for (unsigned i=0; iGetInt(FIELD_FLAGS); if ((Old & MAIL_READ) == 0) { d->SetInt(FIELD_FLAGS, Old | MAIL_READ); Changed.Add(d); } } if (Changed.Length()) OnChange(_FL, Changed, PropId); } break; } default: { LAssert(!"Unsupport prop ID."); return Store3Error; } } return Store3Success; } void LMapiStore::Compact(LViewI *Parent, LDataPropI *Props, std::function OnStatus) { if (OnStatus) OnStatus(true); } void LMapiStore::OnEvent(void *Param) { } bool LMapiStore::OnIdle() { if (Dirty.Length() > 0) { for (unsigned i=0; iMapiMsg) { HRESULT res = t->MapiMsg->SaveChanges(KEEP_OPEN_READWRITE); if (FAILED(res)) { LgiTrace("%s:%i - SaveChanges failed with 0x%x\n", _FL, res); } t->IsDirty = false; } } Dirty.Length(0); } return false; } LDataEventsI *LMapiStore::GetEvents() { return NULL; } ////////////////////////////////////////////////////// LDataStoreI *LMapiThing::GetStore() { return Store; } bool MapiEntryRef::OpenRoot(LPMAPISESSION Session, UI_TYPE UiHnd, IMsgStore **MsgStore, IMAPIFolder **RootFolder) { bool Status = false; if (Session && MsgStore && RootFolder) { if (!*MsgStore) { HRESULT res = Session->OpenMsgStore(UiHnd, (ULONG)Entry.Length(), // entry bytes (LPENTRYID)&Entry[0], // ptr to entry NULL, // default interface: IMsgStore MAPI_BEST_ACCESS, MsgStore); if (FAILED(res)) { Store->Error("%s:%i - OpenMsgStore failed with 0x%x\n", _FL, res); } } if (*MsgStore) { SPropValue *SubTree = MapiGetProp(*MsgStore, PR_IPM_SUBTREE_ENTRYID); if (SubTree) { ULONG ObjType; HRESULT res = (*MsgStore)->OpenEntry(SubTree->Value.bin.cb, (LPENTRYID)SubTree->Value.bin.lpb, NULL, MAPI_MODIFY, &ObjType, (IUnknown**) RootFolder); if (SUCCEEDED(res) && *RootFolder) { Status = true; } else { (*MsgStore)->Release(); *MsgStore = 0; return Store->Error("%s:%i - OpenEntry failed with 0x%x\n", _FL, res); } } } } return Status; } ////////////////////////////////////////////////////// LDataStoreI *OpenMapiStore( const char *Profile, const char *Username, const char *Password, uint64 AccountId, LDataEventsI *Callback) { return new LMapiStore(Profile, Username, Password, AccountId, Callback); } diff --git a/Resources/Scribe.lr8 b/Resources/Scribe.lr8 --- a/Resources/Scribe.lr8 +++ b/Resources/Scribe.lr8 @@ -1,5227 +1,5227 @@ - +