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; 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(); + LString Txt = Clip.Text(); if (Txt) { - LToken t(Txt, "\r\n"); - for (unsigned i=0; iPaste(t[i]); + La->Paste(ln); 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) + void Test(LString::Array &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, " "); + auto t = Txt.SplitDelimit(" "); 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/BayesianFilter.cpp b/Code/BayesianFilter.cpp --- a/Code/BayesianFilter.cpp +++ b/Code/BayesianFilter.cpp @@ -1,1855 +1,1855 @@ #include #include "Scribe.h" #include "resdefs.h" #include "BayesianFilter.h" #include "lgi/common/LgiRes.h" #include "lgi/common/SpellCheck.h" #define SECONDS(n) ((n) * 1000) #define TIMEOUT_BAYES_LOAD SECONDS(10) #define TIMEOUT_SPELL_CHECK SECONDS(3) #define TIMEOUT_UPDATE_REBUILD SECONDS(2) #define TIMEOUT_BAYES_IDLE (50) // ms, out of 100ms idle timer. #define WHITELIST_MY_EMAIL 0 #define WHITELIST_CONTACTS 0 #define STORE_SIZE 16 #define WORD_INTEREST_CENTER 0.5 static const char HamWordsFile[] = "hamwords.idx"; static const char SpamWordsFile[] = "spamwords.idx"; static const char WhiteListFile[] = "whitelist.idx"; void ProcessWords(LString Words, std::function Callback) { char *end = Words.Get() + Words.Length(); if (!Callback) { LAssert(0); return; } for (char *s = Words; s < end; ) { char *e = s; while (*e && *e != ' ' && e < end) e++; if (*e == ' ') *e = 0; Callback(s); s = e + 1; } } double WordInterest(double d) { d -= WORD_INTEREST_CENTER; if (d < 0) d *= -1; return d; } class Token { public: LString Word; double Prob = 0.0; double Interest = 0.0; }; class TokenStore { public: constexpr static int StoreSize = STORE_SIZE; int Used; Token *Tok[StoreSize]; TokenStore() { Used = 0; ZeroObj(Tok); } ~TokenStore() { for (int i=0; i 0) return Tok[Used-1]->Interest; return 0; } double Prob(LStream *Log) { int i; if (Log) { Log->Print("%s\n", LLoadString(IDS_SPAM_PROB)); for (i=0; iPrint("\t[%i] '%s'=%f\n", i, Tok[i]->Word.Get(), Tok[i]->Prob); } } // ab //------------------- //ab + (1 - a)(1 - b) if (Used == 0) return 0.5; double top, bottom; top = Tok[0]->Prob; bottom = 1 - Tok[0]->Prob; for (i=1; iProb; bottom *= 1 - Tok[i]->Prob; } double Result = top / (top + bottom); if (Log) Log->Print("\nResult=%f\n", Result); return Result; } bool CanInsert(double i) { return Used < CountOf(Tok) || i >= Min(); } bool Insert(Token *t) { bool Status = false; if (t) { for (int i=0; iWord, t->Word) == 0) { Status = true; DeleteObj(t); goto End; } else if (t->Interest > Tok[i]->Interest) { if (Used >= CountOf(Tok)) { DeleteObj(Tok[Used-1]); memmove(Tok + i + 1, Tok + i, sizeof(Tok[0]) * (Used - i - 1)); } else { memmove(Tok + i + 1, Tok + i, sizeof(Tok[0]) * (Used - i)); Used++; } Tok[i] = t; Status = true; goto End; } } if (Used < CountOf(Tok)) { Tok[Used++] = t; Status = true; goto End; } else { DeleteObj(t); } } End: return Status; } }; class BayesianThread : public LThread, public LMutex, public LEventTargetI, public LMappedEventSink { public: enum ThreadState { BayesLoading, BayesReady, BayesExiting, }; struct Build { /// Empty all the word databases first... otherwise append new words on top of old ones bool ResetDb; LHashTbl,int> WhiteList; int HamEmailCount; LHashTbl,int> HamWords; int SpamEmailCount; LHashTbl,int> SpamWords; Build(bool Reset) : WhiteList(0, -1), HamWords(0, -1), SpamWords(0, -1) { ResetDb = Reset; HamEmailCount = 0; SpamEmailCount = 0; HamWords.SetMaxSize(HamWords.Unlimited); SpamWords.SetMaxSize(SpamWords.Unlimited); } void InsertWhiteList(const char *Word) { int c = WhiteList.Find(Word); WhiteList.Add(Word, c < 0 ? 1 : c + 1); } void InsertHamWords(const char *Word) { int c = HamWords.Find(Word); HamWords.Add(Word, c < 0 ? 1 : c + 1); } void InsertSpamWords(const char *Word) { int c = SpamWords.Find(Word); SpamWords.Add(Word, c < 0 ? 1 : c + 1); } }; /// This is used when the GUI thread requests an email to be /// tested by the Bayesian Classifier. Initially the GUI thread /// fills out this structure and passes it over to the worker /// thread and it then does the calculation and passes it back /// to the GUI thread for actioning. struct Test { bool Analyse = false; LStringPipe Log; LString FromAddr; LString MsgRef; LString Words; double Score = 0.0; bool WhiteListed = false; }; /// This is a change of classification container for /// when the email changes from ham to spam or the reverse. struct Change { LString Words; LString Str; ScribeMailType OldType = BayesMailUnknown; ScribeMailType NewType = BayesMailUnknown; bool RemoveWhite = false; bool IncrementWhite = false; }; private: ScribeWnd *App; LAutoPtr WhiteList; LAutoPtr Ham; LAutoPtr Spam; ThreadState State; LArray< LAutoPtr > Work; LArray< LAutoPtr > Tests; LArray< LAutoPtr > Results; LArray< LAutoPtr > Changes; LAutoPtr Tokens; uint64_t CheckTextTs = 0; LAutoPtr CheckText; void Empty() { WhiteList.Reset(new LWordStore); Spam.Reset(new LWordStore); Ham.Reset(new LWordStore); #define SetListFile(list, file) \ if (!list->GetFile()) \ { \ if (auto s = FindWordDb(file)) \ { \ list->SetFile(s); \ } \ } SetListFile(WhiteList, WhiteListFile); SetListFile(Spam, SpamWordsFile); SetListFile(Ham, HamWordsFile); // empty the word lists WhiteList->Empty(); WhiteList->Serialize(0, true); Spam->Empty(); Spam->Serialize(0, true); Ham->Empty(); Ham->Serialize(0, true); } public: BayesianThread(ScribeWnd *app) : LThread("BayesianThread.Thread"), LMutex ("BayesianThread.Mutex") { App = app; State = BayesLoading; Run(); } ~BayesianThread() { State = BayesExiting; int64 Start = LCurrentTime(); while (!IsExited()) { LSleep(20); if (LCurrentTime()-Start > 5000) { Terminate(); break; } } } LMessage::Result OnEvent(LMessage *Msg) override { switch (Msg->Msg()) { case M_CHECK_TEXT: CheckTextTs = LCurrentTime(); CheckText.Reset((LSpellCheck::CheckText*)Msg->A()); break; } return 0; } bool PostEvent(int Cmd, LMessage::Param a = 0, LMessage::Param b = 0, int64_t TimeoutMs = -1) override { LMessage m(Cmd, a, b); OnEvent(&m); return true; } void Add(LAutoPtr b) { if (Lock(_FL)) { Work.New() = b; Unlock(); } } void Add(LAutoPtr t) { if (Lock(_FL)) { Tests.New() = t; Unlock(); } } void Add(LAutoPtr c) { if (Lock(_FL)) { Changes.New() = c; Unlock(); } } bool GetResults(LArray< LAutoPtr > &results) { if (Lock(_FL)) { results = Results; Unlock(); } Results.Length(0); return results.Length() > 0; } ThreadState GetState() { return State; } void SetStore(LAutoPtr &s, LWordStore *ws) { if (ws && Lock(_FL)) { s.Reset(ws); Unlock(); } } LString FindWordDb(const char *Name) { LString OptPath; auto Opts = App->GetOptions(); // Look in the same folder as the options file: if (Opts && Opts->GetFile()) { LFile::Path p(Opts->GetFile()); p--; OptPath = p.GetFull(); p += Name; if (p.IsFile()) return p.GetFull(); } // Check the install folder too: LFile::Path p(LSP_APP_INSTALL); p += Name; if (p.IsFile()) return p.GetFull(); // No existing file found, so create a path using the options location: p = OptPath; p += Name; return p.GetFull(); } void OnCheckText(LSpellCheck::CheckText *Ct) { auto p = Ct->Errors.Length() ? 0.3 : 0.7; auto i = WordInterest(p); if (Tokens->CanInsert(i)) { Token *t = new Token; if (t) { t->Word = Ct->Text; t->Prob = p; t->Interest = i; Tokens->Insert(t); } } } double IsSpam(Test *t) { // Check the auto white list if (WhiteList) { long Count = WhiteList->GetWordCount(t->FromAddr); if (Count > 0) { // It's from someone we've accepted mail from before if (t->Analyse) t->Log.Print("%s\n", LLoadString(IDS_IS_WHITELIST)); t->WhiteListed = true; return 0.0; } } bool Analyse = t->Analyse; LAutoPtr Spell(App->CreateSpellObject()); Tokens.Reset(new TokenStore); if (!Ham || !Spam) { LgiTrace("%s:%i - No Ham/Spam DB loaded?\n", _FL); return 0.0; } ssize_t HamItems = Ham->Length(); ssize_t SpamItems = Spam->Length(); if (Analyse) LgiTrace("HamItems=" LPrintfSSizeT " SpamItems=" LPrintfSSizeT "\n"); if (Spell) { Spell->Check(GetHandle(), t->Words, 0, t->Words.Length()); auto Start = LCurrentTime(); while (!CheckText) // Wait for the spell check to complete { if (LCurrentTime() - Start > TIMEOUT_SPELL_CHECK) { LgiTrace("%s:%i - Bayesian filter didn't get response from spell check, continuing without.\n", _FL); break; } LSleep(10); } if (CheckText) LgiTrace("%s:%i - SpellCheck took %ims\n", _FL, (int)(LCurrentTime()-CheckTextTs)); } struct Entry { LString word; ssize_t spam = 0, ham = 0; bool spellErr = false; int Compare(Entry *e) { auto i = (spam+ham) - (e->spam+e->ham); if (i < 0) return -1; return i > 0 ? 1 : 0; } }; LArray entries; ProcessWords(t->Words, [this, t, &entries](auto w) { size_t nextErr = 0; Entry &e = entries.New(); e.word = w; e.spam = Spam->GetWordCount(w); e.ham = Ham->GetWordCount(w); size_t Cur = w - t->Words.Get(); // bool hasAt = Strchr(w, '@') != NULL; if (CheckText) { while (nextErr < CheckText->Errors.Length()) { auto &errs = CheckText->Errors[nextErr]; if (errs.Overlap(Cur)) { e.spellErr = true; nextErr++; break; } if (errs.Start < (ssize_t)Cur) nextErr++; else break; } } }); entries.Sort([](auto a, auto b) { return a->Compare(b); }); for (auto &e: entries) { double s, p, i; if (e.spam && e.ham) { s = (double) e.spam / SpamItems; p = s / ( ((double) e.ham / HamItems) + s); } else if (e.spam) p = 0.99; else if (e.ham) p = e.spellErr ? 0.6 : 0.01; else p = e.spellErr ? 0.88 : 0.4; i = WordInterest(p); if (Tokens->CanInsert(i)) { Token *t = new Token; if (t) { t->Word = e.word; t->Prob = p; t->Interest = i; Tokens->Insert(t); } } if (Analyse) LgiTrace(" %s, " LPrintfSSizeT ", " LPrintfSSizeT ", %i, %g\n", e.word.Get(), e.spam, e.ham, e.spellErr, p); } auto result = Tokens->Prob(&t->Log); if (Analyse) LgiTrace(" result=%g\n", result); return result; } void ConvertHashToBtree(LWordStore *Ws, LHashTbl,int> &Hash, int EmailCount, bool Append, LStream *Debug = NULL) { auto Items = Hash.Length(); int64 Start = LCurrentTime(); if (Debug) Debug->Print("ConvertHashToBtree(%s, %i words, %i emails)\n", Ws->GetFile(), Items, EmailCount); Ws->Empty(); for (auto i: Hash) { ssize_t Result; if (Append) { ssize_t Old = Ws->GetWordCount(i.key); Result = Ws->SetWordCount(i.key, Old + i.value); } else { Result = Ws->SetWordCount(i.key, i.value); } if (!Result) { if (Debug) Debug->Print("SetWordCount(%s, %i) failed\n", i.key, i.value); LAssert(0); return; } } if (EmailCount) { Ws->SetItems(EmailCount); } Hash.Empty(); LgiTrace("ConvertHashToBtree(%s) took %.1f sec, for " LPrintfInt64 " items.\n", Ws->GetFile(), ((double)((int64)LCurrentTime()-Start))/1000.0, Items); } void ApplyChange(Change *c, LWordStore *Ws, bool Add) { bool status = false; if (!Ws || !c) { LgiTrace("%s:%i - Invalid param: %p, %p\n", _FL, c, Ws); return; } ProcessWords(c->Words, [this, Ws, Add, &status](auto w) { ssize_t c = Ws->GetWordCount(w); if (Add) status = Ws->SetWordCount(w, c + 1) > 0; else status = Ws->SetWordCount(w, c > 0 ? c - 1 : 0) > 0; LAssert(status); }); ssize_t Total = Ws->GetItems(); status = Ws->SetItems((int)Total + (Add ? 1 : -1)); LAssert(status); } int Main() override { if (auto s = FindWordDb(HamWordsFile)) SetStore(Ham, new LWordStore(s)); if (auto s = FindWordDb(SpamWordsFile)) SetStore(Spam, new LWordStore(s)); if (auto s = FindWordDb(WhiteListFile)) SetStore(WhiteList, new LWordStore(s)); State = BayesReady; bool Notify = false; while (State == BayesReady) { LAutoPtr b; LAutoPtr t; LAutoPtr c; if (Lock(_FL)) { if (Work.Length()) { b = Work[0]; Work.DeleteAt(0, true); } else if (Tests.Length()) { t = Tests[0]; Tests.DeleteAt(0, true); } else if (Changes.Length()) { c = Changes[0]; Changes.DeleteAt(0, true); } Unlock(); } if (b) { if (b->ResetDb) Empty(); ConvertHashToBtree(Spam, b->SpamWords, b->SpamEmailCount, b->ResetDb == false); ConvertHashToBtree(Ham, b->HamWords, b->HamEmailCount, b->ResetDb == false); ConvertHashToBtree(WhiteList, b->WhiteList, 0, b->ResetDb == false); } else if (t) { t->Score = IsSpam(t); if (Lock(_FL)) { Results.New() = t; Notify = true; Unlock(); } } else if (c) { switch (c->OldType) { default: break; case BayesMailHam: ApplyChange(c, Ham, false); break; case BayesMailSpam: ApplyChange(c, Spam, false); break; } switch (c->NewType) { default: break; case BayesMailHam: ApplyChange(c, Ham, true); break; case BayesMailSpam: ApplyChange(c, Spam, true); break; } if (c->Str) { if (!WhiteList) { LgiTrace("Missing whitelist obj.\n"); } else if (c->NewType == BayesMailSpam || c->RemoveWhite) { // Make sure the email address is not in the white list... WhiteList->DeleteWord(c->Str); LAssert(WhiteList->GetWordCount(c->Str) == 0); } else if (c->IncrementWhite) { auto i = WhiteList->GetWordCount(c->Str); WhiteList->SetWordCount(c->Str, i + 1); } } } else { // No work to do... if (Notify) { App->PostEvent(M_SCRIBE_BAYES_RESULT); Notify = false; } LSleep(20); } } return 0; } }; struct BayesEvent { Mail *m; bool Loading, Done; ScribeMailType OldType, NewType; }; class BuildSpamDB { int FolderLoads = 0; int MailLoads = 0; public: ScribeWnd *App; BayesianFilter *Filter; LAutoPtr Prog; LAutoPtr Debug; // Processing part 1... load the folders: LArray Folders; // Processing part 2... convert the mail to words: struct BuildItem { Mail *m = NULL; bool loading = false; ScribeMailType type = BayesMailUnknown; void Set(Mail *mail, ScribeMailType Type) { m = mail; m->IncRef(); type = Type; loading = false; } }; LArray Items; int HamCount = 0; int SpamCount = 0; int FalsePositives = 0; int FalseNegatives = 0; int LoadFailures = 0; LAutoPtr b; BuildSpamDB(ScribeWnd *app); ~BuildSpamDB(); /// \returns true when the processing is finished bool Process(); void ProcessMail(Mail *m, ScribeMailType type); void AbortProcess(); void AddFolder(ScribeFolder *f) { Folders.Add(f); } bool IsCancelled() { return Prog ? Prog->IsCancelled() : true; } }; class BayesianFilterPriv : public LMutex { LAutoPtr Thread; public: ScribeWnd *App; uint64_t Ts; uint64_t WorkStartTs = 0; LArray Work; LAutoPtr Prog; LAutoPtr Build; BayesianFilterPriv(ScribeWnd *app) : LMutex("BayesianFilterPriv") { Ts = 0; App = app; } BayesianThread *GetThread() { if (!Thread) Thread.Reset(new BayesianThread(App)); return Thread; } bool IsCancelled() { return Build ? Build->IsCancelled() : true; } }; BuildSpamDB::BuildSpamDB(ScribeWnd *app) : App(app), Filter(app) { App->OnFolderTask(Filter->d->GetThread(), true); b.Reset(new BayesianThread::Build(true)); if (Prog.Reset(new LProgressDlg(App))) Prog->SetDescription("Scanning folders..."); LVariant i; if (App->GetOptions()->GetValue(OPT_BayesDebug, i)) { if (Debug.Reset(new LFile)) { char s[MAX_PATH_LEN]; LMakePath(s, sizeof(s), LGetExePath(), "Bayes.txt"); if (Debug->Open(s, O_WRITE)) Debug->SetSize(0); else LgiTrace("%s:%i - Couldn't open '%s'\n", _FL, s); } } } BuildSpamDB::~BuildSpamDB() { // Save stats... LVariant i; auto opts = App->GetOptions(); opts->SetValue(OPT_BayesHam, i = HamCount); opts->SetValue(OPT_BayesSpam, i = SpamCount); opts->SetValue(OPT_BayesFalsePositives, i = FalsePositives); opts->SetValue(OPT_BayesFalseNegitives, i = FalseNegatives); App->OnFolderTask(Filter->d->GetThread(), false); } void BuildSpamDB::AbortProcess() { Folders.Length(0); Items.Length(0); } bool BuildSpamDB::Process() { if (IsCancelled()) AbortProcess(); // This should execute for only a small time slice... if (Folders.Length() || FolderLoads) { if (!Folders.Length()) return false; // Just wait for them... auto f = Folders[0]; Folders.DeleteAt(0); if (f) { auto Path = f->GetPath(); ScribeMailType Type = Filter->BayesTypeFromPath(Path); if (Debug) Debug->Print("%s:%i - add folder '%s', Type=%i\n", _FL, Path.Get(), Type); auto Parent = f->GetParent(); if (Type != BayesMailUnknown && Parent) { FolderLoads++; f->WhenLoaded(_FL, [this, f, Type, Path]() { LgiTrace("%s:%i - Scanning '%s' got %i items, FolderLoads=%i\n", _FL, Path.Get(), (int)f->Items.Length(), FolderLoads); for (auto i: f->Items) { auto m = i->IsMail(); if (!m) continue; // Add item to the work queue... Items.New().Set(m, Type); } FolderLoads--; (*Prog)++; }); // FIXME: does this need to do something on callback? f->LoadThings(); } else { // LgiTrace("%s:%i - Unknown folder '%s'\n", _FL, Path.Get()); (*Prog)++; } } if (Folders.Length() == 0 && FolderLoads == 0) { Prog->SetDescription("Processing mail..."); Prog->SetRange(Items.Length()); Prog->Value(0); } return false; } if (Items.Length() || MailLoads) { if (Items.Length() && MailLoads < 100) { auto Start = LCurrentTime(); while ( (LCurrentTime() - Start) < TIMEOUT_BAYES_IDLE && Items.Length()) { // Process mail items... auto &i = Items[0]; // Operate on read mail only... auto flags = i.m->GetFlags(); if (TestFlag(flags, MAIL_READ)) { MailLoads++; // auto loaded = i.m->GetLoaded(); i.m->WhenLoaded(_FL, [this, mail = i.m, type = i.type] { ProcessMail(mail, type); MailLoads--; (*Prog)++; }); i.m->SetLoaded(); } Items.DeleteAt(0); } } return false; } // We're done... return true; } void BuildSpamDB::ProcessMail(Mail *m, ScribeMailType Type) { auto LoadState = m->GetLoaded(); if (LoadState != Store3Loaded) { LAssert(!"Should only be called on loaded email..."); return; } const char *email; if (Type == BayesMailHam && (email = m->GetFromStr(FIELD_EMAIL))) { b->InsertWhiteList(email); } LString Words; auto Status = Filter->MakeMailWordList(m, Words); if (Status != Store3Success) { LAssert(!"MakeMailWordList failed."); return; } ProcessWords(Words, [this, Type](auto w) { if (Type == BayesMailSpam) b->InsertSpamWords(w); else if (Type == BayesMailHam) b->InsertHamWords(w); // else do nothing }); auto flags = m->GetFlags(); // Remove the bayes DB flags... flags &= ~(MAIL_HAM_DB|MAIL_SPAM_DB); if (Type == BayesMailSpam) { b->SpamEmailCount++; flags |= MAIL_SPAM_DB; } else if (Type == BayesMailHam) { b->HamEmailCount++; flags |= MAIL_HAM_DB; } if (flags & MAIL_BAYES_HAM) // originally classified as ham.. { if (flags & MAIL_SPAM_DB) // but now is spam... FalseNegatives++; else if (Type == BayesMailHam) HamCount++; } else if (flags & MAIL_BAYES_SPAM) // originally classified as spam.. { if (flags & MAIL_SPAM_DB) SpamCount++; else if (Type == BayesMailHam) // but now is ham... FalsePositives++; } // Update the object.. m->SetFlags(flags); // Delete our reference... m->DecRef(); } #ifdef _DEBUG bool HashSerialize(LHashTbl,uint32_t> &h, char *file, bool write) { bool Status = false; struct Field { uint32_t Value; uint16_t Len; char Str[1]; }; LFile f; if (f.Open(file, write?O_WRITE:O_READ)) { Field *fld = (Field*)malloc(64<<10); if (fld) { int header = 6; if (write) { f.SetSize(0); // char *key; // for (int i=h.First(&key); i; i=h.Next(&key)) for (auto i : h) { fld->Value = (uint32_t)i.value; fld->Len = (uint16_t)strlen(i.key); memcpy(fld->Str, i.key, fld->Len + 1); Status = f.Write(fld, header + fld->Len) == (header + fld->Len); } } else { h.Empty(); while (!f.Eof()) { if (f.Read(fld, header) == header) { if (f.Read(fld->Str, fld->Len) == fld->Len) { fld->Str[fld->Len] = 0; Status = h.Add(fld->Str, fld->Value); } else break; } else break; } } free(fld); } } return Status; } #endif BayesianFilter::BayesianFilter(ScribeWnd *app) { d = new BayesianFilterPriv(app); App = app; } BayesianFilter::~BayesianFilter() { DeleteObj(d); } void BayesianFilter::AddFolderToSpamDb(ScribeFolder *f) { if (d->IsCancelled()) return; d->Build->AddFolder(f); for (auto c = f->GetChildFolder(); c; c = c->GetNextFolder()) AddFolderToSpamDb(c); } /* static size_t FolderCount(ScribeFolder *f) { ssize_t len = f->Length(); auto items = f->GetItems(); auto n = MAX(len, items); for (auto c = f->GetChildFolder(); c; c = c->GetNextFolder()) n += FolderCount(c); return n; } */ void BayesianFilter::BuildStats() { auto prob = App->GetFolder("/Mail3/Spam/Probably"); auto inbox = App->GetFolder("/IMAP/INBOX"); if (!prob || !inbox) return; prob->LoadThings(); inbox->LoadThings(); } bool BayesianFilter::BuildSpamDb() { if (!d->GetThread()) return false; // scan folders LVariant MoveTo; if (!App->GetOptions()->GetValue(OPT_BayesMoveTo, MoveTo)) MoveTo = "/Spam/Probably"; if (!d->Build.Reset(new BuildSpamDB(App))) return false; // Recurse over the folders for (auto &s: App->GetStorageFolders()) { if (s.Root) AddFolderToSpamDb(s.Root); } for (auto a : *App->GetAccounts()) { if (a->Receive.GetRootFolder()) AddFolderToSpamDb(a->Receive.GetRootFolder()); } d->Build->Prog->SetRange(d->Build->Folders.Length()); return true; } #define IsCJK(c) \ ( \ ((c)>=0x4E00 && (c)<=0x9FFF) || \ ((c)>=0x3400 && (c)<=0x4DFF) || \ ((c)>=0x20000 && (c)<=0x2A6DF)|| \ ((c)>=0xF900 && (c)<=0xFAFF) || \ ((c)>=0x2F800 && (c)<=0x2FA1F) \ ) bool IsUriChar(int32 ch) { if (!ch) return false; if (IsDigit(ch) || IsAlpha(ch)) return true; if (strchr("-._~:/?#[]@!$&\'()*+,;%=", ch)) return true; return false; } typedef LHashTbl,bool> TokenMap; void TokeniseText(const char *Source, bool *Lut, LString::Array &Blocks, TokenMap *Ignore = NULL) { if (!Source || !Lut) return; char buf[16 << 10]; ssize_t used = 0; auto oldWarn = LUtf8Ptr::Warn; LUtf8Ptr::Warn = false; auto emitBuf = [&]() { if (used > 0) Blocks.New().Set(buf, used); used = 0; }; for (auto *s = Source; *s;) { int32 ch; LUtf8Ptr start(s); // Skip non-word while ( (ch = start) && ch < 256 && !Lut[ch] ) { start++; } // Seek end of word LUtf8Ptr end(start); while ( (ch = end) && ( ch >= 256 || Lut[ch] ) ) { if (end > start && IsCJK(ch)) break; end++; } auto len = end - start; // Is the word something that looks like a URI? bool isUrl = (len == 4 && !Strnicmp((const char*)start.GetPtr(), "http", 4)) || (len == 5 && !Strnicmp((const char*)start.GetPtr(), "https", 5)); if (isUrl) { // Seek to the end of the URI while ((ch = end) && IsUriChar(ch) && (end - start) < sizeof(buf) - 10) end++; isUrl = true; len = end - start; } if (len > 0) { ch = start; if (ch != '*' || len != 10 || Strnicmp((const char*)start.GetPtr(), "***SPAM***", len)) { if (used + len >= sizeof(buf) - 2) emitBuf(); auto word = buf + used; if (isUrl) { LUri uri(LString((char*)start.GetPtr(), len)); if (uri.sHost) { memcpy(word, uri.sHost.Get(), uri.sHost.Length()); used += uri.sHost.Length(); buf[used++] = ' '; } } else { memcpy(word, start.GetPtr(), len); used += len; if (Ignore) { buf[used] = 0; if (Ignore->Find(buf+used-len)) used -= len; // undo adding word else buf[used++] = ' '; // convert to space delimit } else { buf[used++] = ' '; } } } } else { if (ch) LgiTrace("%s:%i - Invalid utf-8, aborting parse...\n", _FL); break; } s = (const char*)end.GetPtr(); } emitBuf(); LUtf8Ptr::Warn = oldWarn; } Store3Status BayesianFilter::MakeMailWordList(Mail *m, LString &out) { LString::Array Blocks; if (!m || !m->GetObject()) return Store3Error; static bool Processing = false; if (!Processing) { Processing = true; // create word lut for deciding whether a char is part of a word or not bool Lut[256]; ZeroObj(Lut); memset(Lut + 'a', true, 'z'-'a'+1); memset(Lut + 'A', true, 'Z'-'A'+1); Lut[(int)'-'] = true; // Lut[(int)'!'] = true; Lut[(int)'$'] = true; bool Email[256]; memcpy(Email, Lut, sizeof(Email)); Email[(int)'@'] = true; Email[(int)'.'] = true; memset(Lut + 0x80, true, 128); // create ignored words list LString::Array Temp; TokenMap Ignore; for (auto a: *App->GetAccounts()) { auto s = a->Identity.Name(); if (ValidStr(s.Str())) TokeniseText(s.Str(), Lut, Temp); s = a->Identity.Email(); if (ValidStr(s.Str())) TokeniseText(s.Str(), Email, Temp); } ProcessWords(LString("").Join(Temp), [&Ignore](auto w) { Ignore.Add(w, true); }); // process various parts of the email TokeniseText(m->GetSubject(), Lut, Blocks, &Ignore); TokeniseText(m->GetFromStr(FIELD_EMAIL), Email, Blocks, &Ignore); TokeniseText(m->GetFromStr(FIELD_NAME), Lut, Blocks, &Ignore); LVariant Body; Store3State Loaded = (Store3State)m->GetObject()->GetInt(FIELD_LOADED); if (Loaded < Store3Loaded) { m->GetBody(); Loaded = (Store3State)m->GetObject()->GetInt(FIELD_LOADED); if (Loaded < Store3Loaded) { Processing = false; return Store3Delayed; } // else continue... } auto Req = m->GetValue("BodyAsText", Body); if (Req) { // auto id = m->GetMessageId(); TokeniseText(Body.Str(), Lut, Blocks, &Ignore); } else { LString path = "(nullFolder)"; auto fld = m->GetFolder(); if (fld) path = fld->GetPath(); LgiTrace("%s:%i - couldn't get body for %s/%s\n", _FL, path.Get(), m->GetMessageId()); /* Technically not an error... body can be blank. Processing = false; return Store3Error; */ } out = LString("").Join(Blocks); Processing = false; return Store3Success; } else { LAssert(!"Recursion."); return Store3Error; } } Store3Status BayesianFilter::IsSpam(double &Result, Mail *m, bool Analyse) { if (!m) { Result = 0.0; return Store3Error; } Store3Status Status = Store3NotImpl; LAutoPtr t(new BayesianThread::Test); const char *FromAddr; if ((FromAddr = m->GetFromStr(FIELD_EMAIL))) { #if WHITELIST_MY_EMAIL // Check if from yourself... if (App->IsMyEmail(FromAddr)) goto OnWhiteListed; #endif // Check the user white list LVariant Wl; if (App->GetOptions()->GetValue(OPT_BayesUserWhiteList, Wl)) { - LToken w(Wl.Str(), "\r\n\t "); + auto w = Wl.LStr().SplitDelimit("\r\n\t "); bool IsWhite = false; for (unsigned i=0; i Contacts; App->GetContacts(Contacts); for (auto c: Contacts) { if (c->HasEmail(FromAddr)) goto OnWhiteListed; } #endif t->FromAddr = FromAddr; } // Tokenise the mail... Status = MakeMailWordList(m, t->Words); if (Status == Store3Error) return Status; if (Status == Store3Delayed) { // Not loaded yet, retry it later... m->WhenLoaded(_FL, [this, Analyse, m]() { double r; IsSpam(r, m, Analyse); }); return Store3Delayed; } // Pass it over to the thread t->Analyse = Analyse; t->MsgRef = m->GetMailRef(); d->GetThread()->Add(t); return Store3Delayed; OnWhiteListed: if (Analyse) OnBayesAnalyse(LLoadString(IDS_IS_WHITELIST), FromAddr); Result = 0.0; return Store3Success; } ScribeMailType BayesianFilter::BayesTypeFromPath(LString Path) { if (!Path) return BayesMailUnknown; auto spam = App->GetFolder(FOLDER_SPAM); auto folder = App->GetFolder(Path); if (spam && folder) { if (folder == spam) return BayesMailSpam; // If folder is a child of the spam folder, it's NOT ham but "unknown". // Typically a staging ground for "possible" spam. So it should not contribute to // bayes word counts. for (auto p = folder->GetParent(); p; p = p->GetParent()) if (p == spam) return BayesMailUnknown; } else { auto t = Path.SplitDelimit("/"); ssize_t spamIdx = -1; for (size_t i=0; i spamIdx + 1 ? BayesMailUnknown : BayesMailSpam; } return BayesMailHam; } ScribeMailType BayesianFilter::BayesTypeFromPath(Mail *m) { ScribeMailType Status = BayesMailUnknown; if (m) { ScribeFolder *f = m->GetFolder(); if (f) Status = BayesTypeFromPath(f->GetPath()); } return Status; } void BayesianFilter::WhiteListIncrement(const char *Word) { if (!Word) return; LAutoPtr c(new BayesianThread::Change); c->IncrementWhite = true; c->Str = Word; d->GetThread()->Add(c); } bool BayesianFilter::RemoveFromWhitelist(const char *Email) { if (!Email) return false; LAutoPtr c(new BayesianThread::Change); c->Str = Email; c->RemoveWhite = true; d->GetThread()->Add(c); return true; } Store3Status BayesianFilter::OnBayesianMailEvent(Mail *m, ScribeMailType OldType, ScribeMailType NewType) { if (!m) return Store3Error; auto flags = m->GetFlags(); if (NewType == BayesMailHam) { if (TestFlag(flags, MAIL_HAM_DB)) return Store3Success; if (!TestFlag(flags, MAIL_BAYES_HAM|MAIL_BAYES_SPAM)) flags |= MAIL_BAYES_HAM; } if (NewType == BayesMailSpam) { if (TestFlag(flags, MAIL_SPAM_DB)) return Store3Success; if (!TestFlag(flags, MAIL_BAYES_HAM|MAIL_BAYES_SPAM)) flags |= MAIL_BAYES_SPAM; } m->SetFlags(flags); if (d->Work.Length() == 0) d->WorkStartTs = LCurrentTime(); auto &w = d->Work.New(); w.m = m; w.m->IncRef(); w.Loading = false; w.Done = false; w.OldType = OldType; w.NewType = NewType; return Store3Delayed; } void BayesianFilter::OnEvent(LMessage *Msg) { switch (Msg->Msg()) { case M_SCRIBE_IDLE: { // LProfile prof("M_SCRIBE_IDLE", 15); if (d->Build) { if (d->Build->Process()) { // Give the hash tables to the worker thread to convert to disk: d->GetThread()->Add(d->Build->b); // And finish doing the build processing: d->Build.Reset(); } break; } auto EmptyWorkQueue = [this]() { for (auto j: d->Work) { if (!j.Done && j.m) { j.m->DecRef(); j.m = NULL; } } d->Work.Length(0); }; // Look for something we can do... auto StartTs = LCurrentTime(); size_t i, processedCount = 0; for (i=0; iWork.Length(); i++) { auto &job = d->Work[i]; if (job.Done) continue; if (!job.m->GetObject()) { // Why? LgiTrace("%s:%i - Error: object not found.\n", _FL); job.Done = true; // We can't continue anyway continue; } if (job.Loading) { // prof.Add("loading"); auto loaded = job.m->GetLoaded(); if (loaded < Store3Loaded) continue; // Still hasn't loaded.. } LString words; // prof.Add("MakeMailWordList"); auto Status = MakeMailWordList(job.m, words); if (Status == Store3Error) { // Remove the work from the list... it's probably going to keep failing d->Work.DeleteAt(i--); continue; } else if (Status == Store3Delayed) { if (job.Loading) LAssert(!"Already waiting for load."); else job.Loading = true; continue; } // prof.Add("change"); LAutoPtr c(new BayesianThread::Change); c->Str = job.m->GetFromStr(FIELD_EMAIL); c->Words = words; c->OldType = job.OldType; c->NewType = job.NewType; d->GetThread()->Add(c); // Update the spam/ham flags... auto flags = job.m->GetFlags(); flags &= ~(MAIL_BAYES_SPAM|MAIL_BAYES_HAM); if (job.NewType == BayesMailHam) flags |= MAIL_BAYES_HAM; else if (job.NewType == BayesMailSpam) flags |= MAIL_BAYES_SPAM; job.m->SetFlags(flags); job.m->DecRef(); job.m = NULL; job.Done = true; processedCount++; if (LCurrentTime()-StartTs >= TIMEOUT_BAYES_IDLE) break; } // prof.Add("post"); if (d->Work.Length() > 0) { if (!processedCount) { EmptyWorkQueue(); } else { auto Now = LCurrentTime(); LAssert(d->WorkStartTs); if (Now - d->WorkStartTs > 300 && !d->Prog) { d->Prog.Reset(new LProgressDlg(App, 500)); d->Prog->SetDescription("Bayesian filtering..."); } if (d->Prog) { d->Prog->SetRange(d->Work.Length()); d->Prog->Value(i); if (d->Prog->IsCancelled()) { // Dump the work queue EmptyWorkQueue(); } } } } else { d->Prog.Reset(); d->WorkStartTs = 0; } break; } case M_SCRIBE_BAYES_RESULT: { LArray< LAutoPtr > Results; if (!d->GetThread()->GetResults(Results)) break; for (auto t: Results) { if (!t) { LAssert(!"Null ptr in Results"); continue; } if (t->Analyse) { LArray a; int Size = (int) t->Log.GetSize(); a.Length(Size+1); t->Log.Read(&a[0], Size); a[Size] = 0; OnBayesAnalyse(&a[0], t->WhiteListed ? t->FromAddr : NULL); } else if (t->MsgRef) { OnBayesResult(t->MsgRef, t->Score); } else LAssert(!"There should always be a msg ref"); } break; } } } diff --git a/Code/Calendar.cpp b/Code/Calendar.cpp --- a/Code/Calendar.cpp +++ b/Code/Calendar.cpp @@ -1,3303 +1,3303 @@ /*hdr ** FILE: Calendar.cpp ** AUTHOR: Matthew Allen ** DATE: 23/11/2001 ** DESCRIPTION: Scribe calender support ** ** Copyright (C) 2001 Matthew Allen ** fret@memecode.com */ #include "Scribe.h" #include "lgi/common/vCard-vCal.h" #include "lgi/common/Combo.h" #include "lgi/common/DateTimeCtrls.h" #include "lgi/common/TabView.h" #include "lgi/common/DisplayString.h" #include "lgi/common/Edit.h" #include "lgi/common/ColourSelect.h" #include "lgi/common/Button.h" #include "lgi/common/LgiRes.h" #include "lgi/common/Json.h" #include "CalendarView.h" #include "PrintContext.h" #include "../Resources/resdefs.h" #include "resource.h" #include "AddressSelect.h" #include "ObjectInspector.h" #define MAX_RECUR 1024 #define DEBUG_REMINDER 0 #define DEBUG_DATES 0 ////////////////////////////////////////////////////////////////////////////// ItemFieldDef CalendarFields[] = { {"Start", SdStart, GV_DATETIME, FIELD_CAL_START_UTC, IDC_START_DATE, 0, true}, {"End", SdEnd, GV_DATETIME, FIELD_CAL_END_UTC, IDC_END_DATE, 0, true}, {"Subject", SdSubject, GV_STRING, FIELD_CAL_SUBJECT, IDC_SUBJECT}, {"Location", SdLocation, GV_STRING, FIELD_CAL_LOCATION, IDC_LOCATION}, {"Show Time As", SdShowTimeAs, GV_INT32, FIELD_CAL_SHOW_TIME_AS, IDC_AVAILABLE_TYPE}, {"All Day", SdAllDay, GV_BOOL, FIELD_CAL_ALL_DAY, IDC_ALL_DAY}, // TRUE if the calendar event recurs {"Recur", SdRecur, GV_INT32, FIELD_CAL_RECUR, -1}, // Base time unit of recurring event. See enum CalRecurFreq: days, weeks, months, years. {"Recur Freq", SdRecurFreq, GV_INT32, FIELD_CAL_RECUR_FREQ, -1}, // Number of FIELD_CAL_RECUR_FREQ units of time between recurring events. (Minimum is '1') {"Recur Interval", SdRecurInterval, GV_INT32, FIELD_CAL_RECUR_INTERVAL, -1}, // Bitfield of days, Bit 0 = Sunday, Bit 1 = Monday, Bit 2 = Teusday etc. {"Filter Days", SdFilterDays, GV_INT32, FIELD_CAL_RECUR_FILTER_DAYS, -1}, // Bitfield of months, Bit 0 = Janurary, Bit 1 = feburary, Bit 2 = March etc. {"Filter Months", SdFilterMonths, GV_INT32, FIELD_CAL_RECUR_FILTER_MONTHS, -1}, // String of year numbers separated by commas: "2010,2014,2018" {"Filter Years", SdFilterYears, GV_STRING, FIELD_CAL_RECUR_FILTER_YEARS, -1}, // Position in month, "1" means the first matching day of the month. Multiple indexes can be joined with ',' // like: "1,3" {"Filter Pos", SdFilterPos, GV_STRING, FIELD_CAL_RECUR_FILTER_POS, -1}, // See the CalRecurEndType enumeration {"Recur End Type", SdRecurEndType, GV_INT32, FIELD_CAL_RECUR_END_TYPE, -1}, // If FIELD_CAL_RECUR_END_TYPE==CalEndOnDate then this specifies the date to end {"Recur End Date", SdRecurEndDate, GV_DATETIME, FIELD_CAL_RECUR_END_DATE, -1}, // If FIELD_CAL_RECUR_END_TYPE==CalEndOnCount then this specifies the count of events {"Recur End Count", SdRecurEndCount, GV_INT32, FIELD_CAL_RECUR_END_COUNT, -1}, // The timezone the start and end are referencing {"Timezone", SdTimeZone, GV_STRING, FIELD_CAL_TIMEZONE, IDC_TIMEZONE}, // User defined notes for the object {"Notes", SdNotes, GV_STRING, FIELD_CAL_NOTES, IDC_DESCRIPTION}, // See the CalendarType enumeration {"Type", SdType, GV_INT32, FIELD_CAL_TYPE, -1}, {"Reminders", SdReminders, GV_STRING, FIELD_CAL_REMINDERS, -1}, {"LastCheck", SdLastCheck, GV_DATETIME, FIELD_CAL_LAST_CHECK, -1}, {0} }; ////////////////////////////////////////////////////////////////////////////// const char *sReminderType[] = { "Email", "Popup", "ScriptCallback" }; const char *sReminderUnits[] = { "Minutes", "Hours", "Days", "Weeks" }; class ReminderItem : public LListItem { CalendarReminderType Type; float Value; CalendarReminderUnits Units; LString Param; public: ReminderItem(CalendarReminderType type, float value, CalendarReminderUnits units, LString param) { Type = type; Value = value; Units = units; Param = param; Update(); SetText("x", 1); } ReminderItem(LString s) { if (!SetString(s)) { // Default Type = CalPopup; Value = 1.0; Units = CalMinutes; Param.Empty(); } Update(); SetText("x", 1); } CalendarReminderType GetType() { return Type; } float GetValue() { return Value; } CalendarReminderUnits GetUnits() { return Units; } LString GetParam() { return Param; } LString GetString() { // See also FIELD_CAL_REMINDERS LString s; s.Printf("%g,%i,%i,%s", Value, Units, Type, Param?LUrlEncode(Param, ",\r\n").Get():""); return s; } bool SetString(LString s) { // See also FIELD_CAL_REMINDERS LString::Array a = s.Split(","); if (a.Length() < 3) return false; Value = (float) a[0].Float(); Units = (CalendarReminderUnits) a[1].Int(); Type = (CalendarReminderType) a[2].Int(); if (a.Length() > 3) Param = LUrlDecode(a[3]); return true; } void Update() { char s[300]; if (Param) sprintf_s(s, sizeof(s), "%s '%s' @ %g %s", sReminderType[Type], Param.Get(), Value, sReminderUnits[Units]); else sprintf_s(s, sizeof(s), "%s @ %g %s", sReminderType[Type], Value, sReminderUnits[Units]); SetText(s); } void OnPaintColumn(LItem::ItemPaintCtx &Ctx, int i, LItemColumn *c) { LColour old = Ctx.Fore; if (i == 1) Ctx.Fore.Rgb(255, 0, 0); LListItem::OnPaintColumn(Ctx, i, c); Ctx.Fore = old; } void OnMouseClick(LMouse &m) { if (m.Down() && m.Left()) { if (GetList()->ColumnAtX(m.x) == 1) { delete this; return; } } LListItem::OnMouseClick(m); } }; //////////////////////////////////////////////////////////////////////////////////// static int DefaultCalenderFields[] = { FIELD_CAL_SUBJECT, FIELD_CAL_START_UTC, FIELD_CAL_END_UTC, 0, }; #define MINUTE 1 // we're in minutes... #define HOUR (60 * MINUTE) #define DAY (24 * HOUR) int ReminderOffsets[] = { 0, 15 * MINUTE, 30 * MINUTE, 1 * HOUR, 2 * HOUR, 3 * HOUR, 4 * HOUR, 5 * HOUR, 6 * HOUR, 7 * HOUR, 8 * HOUR, 9 * HOUR, 10 * HOUR, 11 * HOUR, 12 * HOUR, 1 * DAY, 2 * DAY }; ////////////////////////////////////////////////////////////////////////////// List Calendar::Reminders; int Calendar::DayStart = -1; int Calendar::DayEnd = -1; int Calendar::WorkDayStart = -1; int Calendar::WorkDayEnd = -1; int Calendar::WorkWeekStart = -1; int Calendar::WorkWeekEnd = -1; void InitCalendarView() { Calendar::DayStart = 6; Calendar::DayEnd = 23; Calendar::WorkDayStart = -1; Calendar::WorkDayEnd = -1; Calendar::WorkWeekStart = -1; Calendar::WorkWeekEnd = -1; auto s = LAppInst->GetConfig("Scribe.Calendar.WorkDayStart"); if (s) Calendar::WorkDayStart = (int)s.Int(); s = LAppInst->GetConfig("Scribe.Calendar.WorkDayEnd"); if (s) Calendar::WorkDayEnd = (int)s.Int(); s = LAppInst->GetConfig("Scribe.Calendar.WorkWeekStart"); if (s) Calendar::WorkWeekStart = (int)s.Int() + 1; s = LAppInst->GetConfig("Scribe.Calendar.WorkWeekEnd"); if (s) Calendar::WorkWeekEnd = (int)s.Int() + 1; if (Calendar::WorkDayStart < 0) Calendar::WorkDayStart = 9; if (Calendar::WorkDayEnd < 0) Calendar::WorkDayEnd = 18; if (Calendar::WorkWeekStart < 0) Calendar::WorkWeekStart = 1; if (Calendar::WorkWeekEnd < 0) Calendar::WorkWeekEnd = 5; } void Calendar::CheckReminders() { LDateTime Now; Now.SetNow(); LDateTime Then; Then = Now; Then.AddDays(1); #if DEBUG_REMINDER LgiTrace("%s:%i - Reminders.Len=%i, Now=%s, Then=%s\n", _FL, Reminders.Length(), Now.Get().Get(), Then.Get().Get()); #endif for (auto c: Reminders) { auto App = c->App; LHashTbl,bool> Added; Thing *ReminderThing = NULL; Mail *ReminderEmail = NULL; // Is a reminder on the entry? if (!c->GetObject()) continue; LString Rem = c->GetObject()->GetStr(FIELD_CAL_REMINDERS); if (!Rem) continue; const char *Subj = NULL, *Notes = NULL; c->GetField(FIELD_CAL_SUBJECT, Subj); c->GetField(FIELD_CAL_NOTES, Notes); LArray Times; if (!c->GetTimes(Now, Then, Times)) { #if DEBUG_REMINDER LgiTrace(" No times for '%s'\n", Subj); #endif continue; } LDataI *Obj = c->GetObject(); LDateTime LastCheck = *Obj->GetDate(FIELD_CAL_LAST_CHECK); if (LastCheck.IsValid()) LastCheck.ToLocal(); LString::Array r = Rem.SplitDelimit("\n"); for (unsigned i=0; i LastCheck; bool b = ts <= Now; LgiTrace(" last check = %s\n", LastCheck.Get().Get()); LgiTrace(" now = %s\n", Now.Get().Get()); LgiTrace(" %i %i\n", a, b); #endif if ( ( !LastCheck.IsValid() || ts > LastCheck ) && ts <= Now ) { // Save the last check field now auto NowUtc = Now.Utc(); Obj->SetDate(FIELD_CAL_LAST_CHECK, &NowUtc); c->SetDirty(); // Fire the event switch (ri.GetType()) { case CalEmail: { ScribeAccount *Acc = App->GetCurrentAccount(); if (!Acc || !Acc->Identity.IsValid()) { LView *Ui = c->GetUI(); LgiMsg(Ui ? Ui : c->App, "No from account to use for sending event notifications.", AppName, MB_OK); break; } auto Email = Acc->Identity.Email(); auto Name = Acc->Identity.Name(); LgiTrace("%s:%i - Using account ('%s' '%s') for the event reminder 'from' address.\n", _FL, Email.Str(), Name.Str()); if (!ReminderThing) ReminderThing = App->CreateThingOfType(MAGIC_MAIL); if (!ReminderEmail) { ReminderEmail = ReminderThing ? ReminderThing->IsMail() : NULL; if (ReminderEmail) { LString s; s.Printf("Calendar Notification: %s", Subj); ReminderEmail->SetSubject(s); s.Printf("The event '%s' is due at: %s\n" "\n" "%s\n" "\n" "(This email was generated by Scribe)", Subj, t.s.Get().Get(), Notes ? Notes : ""); ReminderEmail->SetBody(s); auto Frm = ReminderEmail->GetFrom(); Frm->SetStr(FIELD_EMAIL, Email.Str()); Frm->SetStr(FIELD_NAME, Name.Str()); } } if (ReminderEmail) { auto To = ReminderEmail->GetTo(); // Add any custom parameter email address: LString Param = ri.GetParam(); if (LIsValidEmail(Param)) { auto Recip = To->Create(c->GetObject()->GetStore()); if (Recip) { Added.Add(Param, true); Recip->SetStr(FIELD_EMAIL, Param); To->Insert(Recip); } } // Add all the guests LString sGuests = c->GetObject()->GetStr(FIELD_TO); LString::Array Guests = sGuests.SplitDelimit(","); for (auto Guest: Guests) { LAutoString e, n; DecodeAddrName(Guest, n, e, NULL); #if DEBUG_REMINDER LgiTrace("Attendee=%s,%s\n", n.Get(), e.Get()); #endif if (LIsValidEmail(e.Get()) && !Added.Find(e)) { auto Recip = To->Create(c->GetObject()->GetStore()); if (Recip) { if (n) Recip->SetStr(FIELD_NAME, n); Recip->SetStr(FIELD_EMAIL, e); To->Insert(Recip); Added.Add(e, true); } } else { #if DEBUG_REMINDER LgiTrace("Attendee not valid or added already: %s\n", e.Get()); #endif } } } break; } case CalPopup: { if (LgiMsg( 0, // this causes the dialog to be ontop of everything else LLoadString(IDS_EVENT_DUE), AppName, MB_YESNO | MB_SYSTEMMODAL, Subj) == IDYES) { // Open the calendar entry c->DoUI(); } break; } case CalScriptCallback: { break; } default: { LgiTrace("%s:%i - Unknown reminder type.\n", _FL); break; } } } } } } if (ReminderEmail) { ReminderEmail->CreateMailHeaders(); ReminderEmail->Update(); ReminderEmail->Send(true); } } } #define MIN_1 ((int64)LDateTime::Second64Bit * 60) #define HOUR_1 (MIN_1 * 60) #define DAY_1 (HOUR_1 * 24) #define YEAR_1 (DAY_1 * 365) const char *RelativeTime(LDateTime &Then) { static char s[256]; static const int Id[] = { IDS_CAL_LDAY_SUN, IDS_CAL_LDAY_MON, IDS_CAL_LDAY_TUE, IDS_CAL_LDAY_WED, IDS_CAL_LDAY_THU, IDS_CAL_LDAY_FRI, IDS_CAL_LDAY_SAT, }; LDateTime Now; Now.SetNow(); char Val[64]; uint64 n, t; Now.Get(n); Then.Get(t); int64_t Diff = (int64)t - (int64)n; int Yrs = 0; int Months = 0; int Days = 0; int Hrs = 0; int Mins = 0; LDateTime i = Now; int Inc = Then > Now ? 1 : -1; char DirIndcator = Then > Now ? '+' : '-'; while (ABS(Diff) >= YEAR_1) { Yrs++; i.Year(i.Year()+Inc); i.Get(n); Diff = (int64)t - (int64)n; } int TotalDays = 0; if (ABS(Diff) > DAY_1) { TotalDays = Days = (int) (Diff / DAY_1); while (true) { LDateTime first = i; first.AddMonths(Inc); if ( (Inc < 0 && first > Then) // Tracking back in time.. || (Inc > 0 && Then > first) // Forward in time.. ) { Months += Inc; i = first; } else break; } if (Months) { uint64 remaining; i.Get(remaining); Diff = (int64_t)t - (int64_t)remaining; Days = (int) (Diff / DAY_1); } Diff -= (int64) Days * DAY_1; } if (ABS(Diff) > HOUR_1) { Hrs = (int) (Diff / HOUR_1); Diff -= (int64) Hrs * HOUR_1; } if (ABS(Diff) > MIN_1) { Mins = (int) (Diff / MIN_1); Diff -= (int64) Mins * MIN_1; } if (Yrs) { // Years + months sprintf_s(Val, sizeof(Val), "%c%iy %im", DirIndcator, abs(Yrs), abs(Months)); } else if (Months) { // Months + days sprintf_s(Val, sizeof(Val), "%c%im %id", DirIndcator, abs(Months), abs(Days)); } else if (Days) { if (abs(Days) >= 7) { // Weeks + days... sprintf_s(Val, sizeof(Val), "%c%iw %id", DirIndcator, abs(Days)/7, abs(Days)%7); } else { // Days + hours... sprintf_s(Val, sizeof(Val), "%c%id %ih", DirIndcator, abs(Days), abs(Hrs)); } } else if (Hrs) { // Hours + min sprintf_s(Val, sizeof(Val), "%c%ih %im", DirIndcator, abs(Hrs), abs(Mins)); } else { // Mins sprintf_s(Val, sizeof(Val), "%c%im", DirIndcator, abs(Mins)); } if (Yrs != 0 || Months != 0) { sprintf_s(s, sizeof(s), "%s", Val); return s; } auto NowDay = n / DAY_1; auto ThenDay = t / DAY_1; auto DaysDiff = (int64_t)ThenDay - (int64_t)NowDay; int Ch = 0; if (NowDay == ThenDay) Ch = sprintf_s(s, sizeof(s), "%s", LLoadString(IDS_TODAY)); else if (DaysDiff == -1) Ch = sprintf_s(s, sizeof(s), "%s", LLoadString(IDS_YESTERDAY)); else if (DaysDiff == 1) Ch = sprintf_s(s, sizeof(s), "%s", LLoadString(IDS_TOMORROW)); else if (DaysDiff > 1 && DaysDiff < 7) Ch = sprintf_s(s, sizeof(s), LLoadString(IDS_THIS_WEEK), LLoadString(Id[Then.DayOfWeek()])); else if (DaysDiff >= 7 && DaysDiff < 14) Ch = sprintf_s(s, sizeof(s), LLoadString(IDS_NEXT_WEEK), LLoadString(Id[Then.DayOfWeek()])); else { sprintf_s(s, sizeof(s), "%s", Val); return s; } sprintf_s(s+Ch, sizeof(s)-Ch, ", %s", Val); return s; } int CalSorter(TimePeriod *a, TimePeriod *b) { return a->s.Compare(&b->s); } bool Calendar::SummaryOfToday(ScribeWnd *App, LVariant &v) { bool Status = false; LArray Sources; if (App && App->GetCalendarSources(Sources)) { LDateTime Now; Now.SetNow(); LDateTime Next = Now; Next.AddMonths(1); LArray e; for (unsigned i=0; iGetEvents(Now, Next, e); } if (e.Length()) { e.Sort(CalSorter); LStringPipe p; p.Print("\n"); for (unsigned n=0; nGetField(FIELD_CAL_SUBJECT, Subject); LColour Base32 = c->GetColour(); auto Edge = Base32.Mix(LColour(L_WORKSPACE), 0.85f); sprintf_s(Back, sizeof(Back), "#%2.2x%2.2x%2.2x", Edge.r(), Edge.g(), Edge.b()); sprintf_s(Fore, sizeof(Fore), "#%2.2x%2.2x%2.2x", Base32.r(), Base32.g(), Base32.b()); p.Print("\t
\n" "\t\t%s\n", Fore, Back, Fore, Fore, (Thing*)c, Subject); char Str[256]; const char *Rel = RelativeTime(tp.s); if (Rel) { p.Print("\t\t
%s\n", Rel); } tp.s.Get(Str, sizeof(Str)); p.Print("\t\t
%s\n", Str); tp.e.Get(Str, sizeof(Str)); p.Print("\t\t-
%s\n", Str); } p.Print("
\n"); char *Str = p.NewStr(); if (Str) { v = Str; DeleteArray(Str); Status = true; } } else { char s[256]; sprintf_s(s, sizeof(s), "%s", LLoadString(IDS_NO_EVENTS)); v = s; Status = true; } } return Status; } void Calendar::OnSerialize(bool Write) { // Update reminder list.. Reminders.Delete(this); const char *Rem = NULL; if (GetField(FIELD_CAL_REMINDERS, Rem) && ValidStr(Rem)) { Reminders.Insert(this); } SetImage(GetCalType() == CalTodo ? ICON_TODO : ICON_CALENDAR); if (Write && TodoView) { TodoView->Update(); TodoView->Resort(); } } ////////////////////////////////////////////////////////////////////////////// Calendar::Calendar(ScribeWnd *app, LDataI *object) : Thing(app, object) { DefaultObject(object); Ui = 0; TodoView = 0; Source = 0; SetImage(ICON_CALENDAR); } Calendar::~Calendar() { DeleteObj(TodoView); Reminders.Delete(this); } bool Calendar::GetTimes(LDateTime StartLocal, LDateTime EndLocal, LArray &Times) { if (Calendar::DayStart < 0) InitCalendarView(); ssize_t StartLen = Times.Length(); LDateTime StartUtc = StartLocal; LDateTime EndUtc = EndLocal; StartUtc.ToUtc(); EndUtc.ToUtc(); // int StartTz = StartLocal.GetTimeZone(); TimePeriod w; w.s = StartUtc; w.e = EndUtc; const char *Subj = NULL; GetField(FIELD_CAL_SUBJECT, Subj); TimePeriod BaseUtc; LArray Periods; if (GetField(FIELD_CAL_START_UTC, BaseUtc.s)) { if (!GetField(FIELD_CAL_END_UTC, BaseUtc.e)) { BaseUtc.e = BaseUtc.s; BaseUtc.e.AddHours(1); } LArray Dst; LDateTime::GetDaylightSavingsInfo(Dst, BaseUtc.s, &EndUtc); LAssert(Dst.Length() > 0); Periods.Add(BaseUtc); LDateTime BaseS = BaseUtc.s; LDateTime::DstToLocal(Dst, BaseS); auto BaseTz = BaseS.GetTimeZone(); // Process recur rules int Recur = 0; if (GetField(FIELD_CAL_RECUR, Recur) && Recur) { LDateTime Diff = BaseUtc.e - BaseUtc.s; int FilterFreq = -1; int FilterInterval = 0; int FilterDay = 0; int FilterMonth = 0; const char *FilterYear = 0; const char *FilterPos = 0; int EndType = 0; LDateTime EndDate; int EndCount = 0; GetField(FIELD_CAL_RECUR_FREQ, FilterFreq); GetField(FIELD_CAL_RECUR_INTERVAL, FilterInterval); GetField(FIELD_CAL_RECUR_FILTER_DAYS, FilterDay); GetField(FIELD_CAL_RECUR_FILTER_MONTHS, FilterMonth); GetField(FIELD_CAL_RECUR_FILTER_YEARS, FilterYear); GetField(FIELD_CAL_RECUR_FILTER_POS, FilterPos); GetField(FIELD_CAL_RECUR_END_TYPE, EndType); GetField(FIELD_CAL_RECUR_END_DATE, EndDate); GetField(FIELD_CAL_RECUR_END_COUNT, EndCount); LDateTime CurUtc = BaseUtc.s; const char *Error = 0; int Count = 0; while (!Error) { // Advance the current date by interval * freq switch (FilterFreq) { case CalFreqDays: CurUtc.AddDays(FilterInterval); break; case CalFreqWeeks: CurUtc.AddDays(FilterInterval * 7); break; case CalFreqMonths: CurUtc.AddMonths(FilterInterval); break; case CalFreqYears: CurUtc.Year(CurUtc.Year() + FilterInterval); break; default: Error = "Invalid freq."; break; } if (Error || CurUtc > EndUtc) break; // Check against end conditions bool IsEnded = CurUtc > EndUtc; switch (EndType) { case CalEndNever: break; case CalEndOnCount: // count IsEnded = Count >= EndCount - 1; break; case CalEndOnDate: // date IsEnded = CurUtc > EndDate; break; default: // error IsEnded = true; break; } if (IsEnded) break; // Check against filters LDateTime CurLocal = CurUtc; LDateTime::DstToLocal(Dst, CurLocal); // This fixes the current time when it's in a different daylight saves zone. // Otherwise you get events one hour or whatever out of position after DST starts // or ends during the recurring set. int DiffMins = BaseTz - CurLocal.GetTimeZone(); CurLocal.AddMinutes(DiffMins); bool Show = true; if (FilterDay) { int Day = CurLocal.DayOfWeek(); for (int i=0; i<7; i++) { int Bit = 1 << i; if (Day == i && (FilterDay & Bit) == 0) { Show = false; break; } } } if (Show && FilterMonth) { for (int i=0; i<12; i++) { int Bit = 1 << i; if ((CurLocal.Month() == i + 1) && (FilterMonth & Bit) == 0) { Show = false; break; } } } if (Show && ValidStr(FilterYear)) { - LToken t(FilterYear, " ,;:"); + auto t = LString(FilterYear).SplitDelimit(" ,;:"); Show = false; for (unsigned i=0; i= MAX_RECUR) break; TimePeriod &p = Periods.New(); p.s = CurLocal; p.e = CurLocal; p.e.AddHours(Diff.Hours()); p.e.AddMinutes(Diff.Minutes()); } Count++; } } // Now process periods into multiday segments if needed for (unsigned k=0; k Calendar::WorkDayEnd) { t.e.Hours(23); t.e.Minutes(59); t.e.Seconds(59); } else { t.e.Hours(Calendar::WorkDayEnd); } if (t.Overlap(w)) { t.c = this; Times.Add(t); } } else if (i.IsSameDay(n.e)) { // End day TimePeriod t; t.s = n.e; t.e = n.e; int h = t.s.Hours(); if (h < Calendar::WorkDayStart) { t.s.Hours(0); t.s.Minutes(0); t.s.Seconds(0); } else { t.s.Hours(Calendar::WorkDayStart); } if (t.Overlap(w)) { t.c = this; Times.Add(t); } break; } else { // Middle day TimePeriod t; t.s = i; t.s.Hours(Calendar::WorkDayStart); t.s.Minutes(0); t.s.Seconds(0); t.e = i; t.e.Hours(Calendar::WorkDayEnd); t.e.Minutes(0); t.e.Seconds(0); if (t.Overlap(w)) { t.c = this; Times.Add(t); } } } } else if (n.Overlap(w)) { n.c = this; Times.Add(n); } } } return (int)Times.Length() > StartLen; } CalendarType Calendar::GetCalType() { CalendarType Type = CalEvent; GetField(FIELD_CAL_TYPE, (int&)Type); return Type; } void Calendar::SetCalType(CalendarType Type) { SetField(FIELD_CAL_TYPE, (int)Type); SetImage(Type == CalTodo ? ICON_TODO : ICON_CALENDAR); } LColour Calendar::GetColour() { if (GetObject()) { int64 c = GetObject()->GetInt(FIELD_COLOUR); if (c >= 0) { return LColour((uint32_t)c, 32); } } if (Source) return Source->GetColour(); return LColour(L_LOW); } void Calendar::OnPaintView(LSurface *pDC, LFont *Font, LRect *Pos, TimePeriod *Period) { LRect p = *Pos; const char *Title = "..."; LDateTime Now; float Sx = 1.0; float Sy = 1.0; if (pDC->IsPrint()) { auto DcDpi = pDC->GetDpi(); auto SrcDpi = LScreenDpi(); Sx = (float)DcDpi.x / SrcDpi.x; Sy = (float)DcDpi.y / SrcDpi.y; } float Scale = Sx < Sy ? Sx : Sy; Now.SetNow(); GetField(FIELD_CAL_SUBJECT, Title); bool Delayed = GetObject() ? GetObject()->GetInt(FIELD_STATUS) == Store3Delayed : false; LColour Grey(192, 192, 192); auto View = GetView(); if (View && View->Selection.HasItem(this)) { // selected LColour f = L_FOCUS_SEL_FORE; LColour b = L_FOCUS_SEL_BACK; if (Delayed) { f = f.Mix(Grey); b = b.Mix(Grey); } pDC->Colour(f); pDC->Box(&p); p.Inset(1, 1); pDC->Colour(b); Font->Colour(f, b); } else { LColour Text(0x80, 0x80, 0x80); LColour Base = GetColour(); LColour Qtr = GdcMixColour(Base, LColour(L_WORKSPACE), 0.1f); LColour Half = GdcMixColour(Base, LColour(L_WORKSPACE), 0.4f); // not selected LColour f, b; if (Period && Now < Period->s) { // future entry (full colour) f = Base; b = Half; } else { // historical entry (half strength colour) f = Half; b = Qtr; } if (Delayed) { f = f.Mix(Grey); b = b.Mix(Grey); } pDC->Colour(f); pDC->Box(&p); p.Inset(1, 1); pDC->Colour(b); Font->Colour(Text, b); } pDC->Rectangle(&p); p.Inset((int)SX(1), (int)SY(1)); Font->Transparent(false); LDisplayString ds(Font, Title); float Ht = p.Y() > 0 ? (float)ds.Y() / p.Y() : 1.0f; if (Ht < 0.75f) p.Inset((int)SX(3), (int)SY(3)); else if (Ht < 0.95f) p.Inset((int)SX(1), (int)SY(1)); ds.Draw(pDC, p.x1, p.y1, &p); ViewPos.Union(Pos); } Thing &Calendar::operator =(Thing &Obj) { if (Obj.GetObject() && GetObject()) { GetObject()->CopyProps(*Obj.GetObject()); } return *this; } bool Calendar::operator ==(Thing &t) { Calendar *c = t.IsCalendar(); if (!c) return false; { LDateTime a, b; if (GetField(FIELD_CAL_START_UTC, a) && c->GetField(FIELD_CAL_START_UTC, b) && a != b) return false; if (GetField(FIELD_CAL_END_UTC, a) && c->GetField(FIELD_CAL_END_UTC, b) && a != b) return false; } { const char *a, *b; if (GetField(FIELD_CAL_SUBJECT, a) && c->GetField(FIELD_CAL_SUBJECT, b) && Stricmp(a, b)) return false; if (GetField(FIELD_CAL_LOCATION, a) && c->GetField(FIELD_CAL_LOCATION, b) && Stricmp(a, b)) return false; } return true; } int Calendar::Compare(LListItem *Arg, ssize_t FieldId) { Calendar *c1 = this; Calendar *c2 = dynamic_cast(Arg); if (c1 && c2) { switch (FieldId) { case FIELD_CAL_START_UTC: case FIELD_CAL_END_UTC: { LDateTime d1, d2; if (!c1->GetField((int)FieldId, d1)) d1.SetNow(); if (!c2->GetField((int)FieldId, d2)) d2.SetNow(); return d1.Compare(&d2); break; } case FIELD_CAL_SUBJECT: case FIELD_CAL_LOCATION: { const char *s1 = "", *s2 = ""; if (c1->GetField((int)FieldId, s1) && c2->GetField((int)FieldId, s2)) { return _stricmp(s1, s2); } break; } } } return 0; } uint32_t Calendar::GetFlags() { return 0; } ThingUi *Calendar::DoUI(MailContainer *c) { if (!Ui) { Ui = new CalendarUi(this); } return Ui; } ThingUi *Calendar::GetUI() { return Ui; } bool Calendar::SetUI(ThingUi *ui) { if (Ui) Ui->Item = NULL; Ui = dynamic_cast(ui); if (Ui) Ui->Item = this; return true; } void Calendar::OnMouseClick(LMouse &m) { if (m.Down()) { if (m.Left()) { if (m.Double()) { DoUI(); } } else if (m.Right()) { auto View = GetView(); DoContextMenu(m, View ? (LView*)View : (LView*)App); } } } void Calendar::OnCreate() { for (auto v: CalendarView::CalendarViews) v->OnContentsChanged(); } bool Calendar::OnDelete() { bool IsTodo = GetCalType() == CalTodo; bool Status = Thing::OnDelete(); if (IsTodo) DeleteObj(TodoView); for (auto v: CalendarView::CalendarViews) v->OnContentsChanged(); return Status; } bool Calendar::GetParentSelection(LList *Lst, List &s) { if (Lst) { return Lst->GetSelection(s); } if (auto View = GetView()) { LArray &a = View->GetSelection(); for (unsigned i=0; i(a[i])); } return true; } return false; } #define IDM_MOVE_TO 4000 void Calendar::DoContextMenu(LMouse &m, LView *Parent) { LSubMenu Sub; LScriptUi s(&Sub); if (s.Sub) { s.Sub->AppendItem(LLoadString(IDS_OPEN), IDM_OPEN); s.Sub->AppendItem(LLoadString(IDS_DELETE), IDM_DELETE); s.Sub->AppendItem(LLoadString(IDS_EXPORT), IDM_EXPORT); s.Sub->AppendSeparator(); s.Sub->AppendItem(LLoadString(IDS_INSPECT), IDM_INSPECT); CalendarView *Cv = dynamic_cast(Parent); if (Cv) { s.Sub->AppendSeparator(); for (unsigned n = 0; nGetName()); s.Sub->AppendItem(m, IDM_MOVE_TO + n++, Source != Cs); } } LArray Callbacks; if (App->GetScriptCallbacks(LThingContextMenu, Callbacks)) { LScriptArguments Args(NULL); Args[0] = new LVariant(App); Args[1] = new LVariant(this); Args[2] = new LVariant(&s); for (unsigned i=0; iExecuteScriptCallback(*Callbacks[i], Args); Args.DeleteObjects(); } m.ToScreen(); int Result = s.Sub->Float(App, m.x, m.y); switch (Result) { default: { if (Cv) { int Idx = Result - IDM_MOVE_TO; if (Idx >= 0 && Idx < (int)CalendarSource::GetSources().Length()) { CalendarSource *Dst = CalendarSource::GetSources().ItemAt(Idx); if (Dst) { FolderCalendarSource *Fsrc = dynamic_cast(Dst); if (Fsrc) { const char *Path = Fsrc->GetPath(); ScribeFolder *DstFolder = App->GetFolder(Path); if (DstFolder) { LArray Items; Items.Add(this); DstFolder->MoveTo(Items); if (Items[0] != (Thing*)this) return; Source = Dst; Cv->Invalidate(); } } else LAssert(!"Impl me."); } } } // Handle any installed callbacks for menu items for (unsigned i=0; iExecuteScriptCallback(Cb, Args); } } break; } case IDM_OPEN: { DoUI(); break; } case IDM_DELETE: { LVariant ConfirmDelete; App->GetOptions()->GetValue(OPT_ConfirmDelete, ConfirmDelete); if (!ConfirmDelete.CastInt32() || LgiMsg(Parent, LLoadString(IDS_DELETE_ASK), AppName, MB_YESNO) == IDYES) { List Del; LList *PList = dynamic_cast(Parent); if (GetParentSelection(PList ? PList : GetList(), Del)) { for (auto i: Del) { Thing *t = dynamic_cast(i); if (t) { t->OnDelete(); } else { CalendarTodoItem *Todo = dynamic_cast(i); if (Todo) { Calendar *c = Todo->GetTodo(); c->OnDelete(); } } } } else { Thing *t = this; t->OnDelete(); } } break; } case IDM_EXPORT: { ExportAll(GetList(), sMimeVCalendar, NULL); break; } case IDM_INSPECT: { new ObjectInspector(App, this); break; } } } } const char *Calendar::GetText(int i) { static char s[64]; if (!GetObject()) { LAssert(!"No storage object"); return 0; } int Field = 0; if (FieldArray.Length()) { if (i >= 0 && i < (int) FieldArray.Length()) Field = FieldArray[i]; } else if (i >= 0 && i < CountOf(DefaultCalenderFields)) { Field = DefaultCalenderFields[i]; } if (!Field) return 0; ItemFieldDef *Def = GetFieldDefById(Field); if (!Def) { LAssert(!"Where is the field def?"); return 0; } switch (Def->Type) { case GV_STRING: { return GetObject()->GetStr(Field); break; } case GV_INT32: { sprintf_s(s, sizeof(s), LPrintfInt64, GetObject()->GetInt(Field)); return s; break; } case GV_DATETIME: { // Get any effective timezone for this event. LString Tz = GetObject()->GetStr(FIELD_CAL_TIMEZONE); auto dt = GetObject()->GetDate(Field); if (dt && dt->IsValid()) { LDateTime tmp = *dt; #if DEBUG_DATES printf("GetText.UTC %i = %s\n", i, tmp.Get().Get()); #endif bool UseLocal = true; tmp.SetTimeZone(0, false); if (Tz.Get()) { bool HasPt = false, HasDigit = false; char *e = Tz.Get(); while (strchr(" \t\r\n-.+", *e) || IsDigit(*e)) { if (*e == '.') HasPt = true; if (IsDigit(*e)) HasDigit = true; e++; } if (HasDigit) { if (HasPt) { double TzHrs = Tz.Float(); double i, f = modf(TzHrs, &i); int Mins = (int) ((i * 60) + (f * 60)); tmp.AddMinutes(Mins); } else { int64 i = Tz.Int(); int a = (int)ABS(i); int Mins = (int) (((a / 100) * 60) + (a % 100)); tmp.AddMinutes(i < 0 ? -Mins : Mins); } UseLocal = false; } } if (UseLocal) tmp.ToLocal(); #if DEBUG_DATES printf("GetText.Local %i = %s\n", i, tmp.Get().Get()); #endif tmp.Get(s, sizeof(s)); return s; } break; } default: { LAssert(0); break; } } return 0; } int *Calendar::GetDefaultFields() { return DefaultCalenderFields; } const char *Calendar::GetFieldText(int Field) { return 0; } bool Calendar::Overlap(Calendar *c) { if (c) { LDateTime Ts, Te, Cs, Ce; if (GetField(FIELD_CAL_START_UTC, Ts) && c->GetField(FIELD_CAL_START_UTC, Cs)) { if (!GetField(FIELD_CAL_END_UTC, Te)) { Te = Ts; Te.AddHours(1); } if (!c->GetField(FIELD_CAL_END_UTC, Ce)) { Ce = Cs; Ce.AddHours(1); } if ((Ce <= Ts) || (Cs >= Te)) { return false; } return true; } } return false; } bool Calendar::Save(ScribeFolder *Folder) { bool Status = false; // Check the dates are the right way around LDateTime Start, End; if (GetField(FIELD_CAL_START_UTC, Start) && GetField(FIELD_CAL_END_UTC, End)) { if (End < Start) { SetField(FIELD_CAL_START_UTC, End); SetField(FIELD_CAL_END_UTC, Start); } } auto ChangeEvent = [this](bool Status) { auto View = GetView(); if (View && Status) { View->OnContentsChanged(Source); OnSerialize(true); } }; if (GetObject() && GetObject()->GetInt(FIELD_STORE_TYPE) == Store3Webdav) { auto ParentObj = Folder ? Folder->GetObject() : NULL; Store3Status s = GetObject()->Save(ParentObj); Status = s > Store3Error; if (Status) SetDirty(false); ChangeEvent(Status); } else { if (!Folder) { Folder = GetFolder(); } if (!Folder && App) { Folder = App->GetFolder(FOLDER_CALENDAR); } // FIXME: This can't wait for WriteThing to finish it's call back... Status = true; if (Folder) { LDateTime Now; GetObject()->SetDate(FIELD_DATE_MODIFIED, &Now.SetNow()); Folder->WriteThing(this, [this, ChangeEvent](auto Status) { if (Status > Store3Error) SetDirty(false); ChangeEvent(Status); }); } else ChangeEvent(Status); } return Status; } // Import/Export bool Calendar::GetFormats(bool Export, LString::Array &MimeTypes) { MimeTypes.Add(sMimeVCalendar); return MimeTypes.Length() > 0; } Thing::IoProgress Calendar::Import(IoProgressImplArgs) { if (Stricmp(mimeType, sMimeVCalendar) && Stricmp(mimeType, sMimeICalendar)) IoProgressNotImpl(); VCal vCal; if (!vCal.Import(GetObject(), stream)) IoProgressError("vCal import failed."); IoProgressSuccess(); } Thing::IoProgress Calendar::Export(IoProgressImplArgs) { if (Stricmp(mimeType, sMimeVCalendar)) IoProgressNotImpl(); VCal vCal; if (!vCal.Export(GetObject(), stream)) IoProgressError("vCal export failed."); IoProgressSuccess(); } char *Calendar::GetDropFileName() { if (!DropFileName) { const char *Name = 0; GetField(FIELD_CAL_SUBJECT, Name); DropFileName.Reset(MakeFileName(Name ? Name : "Cal", "ics")); } return DropFileName; } bool Calendar::GetDropFiles(LString::Array &Files) { bool Status = false; if (GetDropFileName()) { if (!LFileExists(DropFileName)) { LAutoPtr F(new LFile); if (F->Open(DropFileName, O_WRITE)) { F->SetSize(0); Export(AutoCast(F), sMimeVCalendar); } } if (LFileExists(DropFileName)) { Files.Add(DropFileName.Get()); Status = true; } } return Status; } void Calendar::OnPrintHeaders(ScribePrintContext &Context) { LDisplayString *ds = Context.Text(LLoadString(IDS_CAL_EVENT)); LRect &r = Context.MarginPx; int Line = ds->Y(); LDrawListSurface *Page = Context.Pages.Last(); Page->Rectangle(r.x1, Context.CurrentY + (Line * 5 / 10), r.x2, Context.CurrentY + (Line * 6 / 10)); Context.CurrentY += Line; } void Calendar::OnPrintText(ScribePrintContext &Context, LPrintPageRanges &Pages) { // Print document for (ItemFieldDef *Fld = CalendarFields; Fld->FieldId; Fld++) { LString Value; switch (Fld->Type) { case GV_STRING: { Value = GetObject()->GetStr(Fld->FieldId); break; } case GV_DATETIME: { auto Dt = GetObject()->GetDate(Fld->FieldId); if (Dt) { char s[64] = ""; Dt->Get(s, sizeof(s)); Value = s; } break; } default: break; } if (Value) { LString f; const char *Name = LLoadString(Fld->FieldId); f.Printf("%s: %s", Name ? Name : Fld->DisplayText, Value.Get()); // LDisplayString *ds = Context.Text(f); } } } bool Calendar::GetVariant(const char *Name, LVariant &Value, const char *Array) { ScribeDomType Fld = StrToDom(Name); switch (Fld) { // String variables case SdSubject: // Type: String Value = GetObject()->GetStr(FIELD_CAL_SUBJECT); break; case SdTo: // Type: String Value = GetObject()->GetStr(FIELD_TO); break; case SdLocation: // Type: String Value = GetObject()->GetStr(FIELD_CAL_LOCATION); break; case SdUid: // Type: String Value = GetObject()->GetStr(FIELD_UID); break; case SdReminders: // Type: String Value = GetObject()->GetStr(FIELD_CAL_REMINDERS); break; case SdNotes: // Type: String Value = GetObject()->GetStr(FIELD_CAL_NOTES); break; case SdStatus: // Type: String Value = GetObject()->GetStr(FIELD_CAL_STATUS); break; // Int variables case SdType: // Type: Int32 Value = GetObject()->GetInt(FIELD_CAL_TYPE); break; case SdCompleted: // Type: Int32 Value = GetObject()->GetInt(FIELD_CAL_COMPLETED); break; case SdShowTimeAs: // Type: Int32 Value = GetObject()->GetInt(FIELD_CAL_SHOW_TIME_AS); break; case SdRecur: // Type: Int32 Value = GetObject()->GetInt(FIELD_CAL_RECUR); break; case SdRecurFreq: // Type: Int32 Value = GetObject()->GetInt(FIELD_CAL_RECUR_FREQ); break; case SdRecurInterval: // Type: Int32 Value = GetObject()->GetInt(FIELD_CAL_RECUR_INTERVAL); break; case SdRecurEndCount: // Type: Int32 Value = GetObject()->GetInt(FIELD_CAL_RECUR_END_COUNT); break; case SdRecurEndType: // Type: Int32 Value = GetObject()->GetInt(FIELD_CAL_RECUR_END_TYPE); break; case SdRecurFilterDays: // Type: Int32 Value = GetObject()->GetInt(FIELD_CAL_RECUR_FILTER_DAYS); break; case SdRecurFilterMonths: // Type: Int32 Value = GetObject()->GetInt(FIELD_CAL_RECUR_FILTER_MONTHS); break; case SdPrivacy: // Type: Int32 Value = GetObject()->GetInt(FIELD_CAL_PRIVACY); break; case SdAllDay: // Type: Int32 Value = GetObject()->GetInt(FIELD_CAL_ALL_DAY); break; case SdColour: // Type: Int64 Value = GetObject()->GetInt(FIELD_COLOUR); break; // Date time fields case SdStart: // Type: DateTime return GetDateField(FIELD_CAL_START_UTC, Value); case SdEnd: // Type: DateTime return GetDateField(FIELD_CAL_END_UTC, Value); case SdDateModified: // Type: DateTime return GetDateField(FIELD_DATE_MODIFIED, Value); default: return false; } return true; } bool Calendar::SetVariant(const char *Name, LVariant &Value, const char *Array) { ScribeDomType Fld = StrToDom(Name); switch (Fld) { // String variables case SdSubject: Value = GetObject()->SetStr(FIELD_CAL_SUBJECT, Value.Str()); break; case SdTo: Value = GetObject()->SetStr(FIELD_TO, Value.Str()); break; case SdLocation: Value = GetObject()->SetStr(FIELD_CAL_LOCATION, Value.Str()); break; case SdUid: Value = GetObject()->SetStr(FIELD_UID, Value.Str()); break; case SdReminders: Value = GetObject()->SetStr(FIELD_CAL_REMINDERS, Value.Str()); break; case SdNotes: Value = GetObject()->SetStr(FIELD_CAL_NOTES, Value.Str()); break; case SdStatus: Value = GetObject()->SetStr(FIELD_CAL_STATUS, Value.Str()); break; // Int variables case SdType: Value = GetObject()->SetInt(FIELD_CAL_TYPE, Value.CastInt32()); break; case SdCompleted: Value = GetObject()->SetInt(FIELD_CAL_COMPLETED, Value.CastInt32()); break; case SdShowTimeAs: Value = GetObject()->SetInt(FIELD_CAL_SHOW_TIME_AS, Value.CastInt32()); break; case SdRecur: Value = GetObject()->SetInt(FIELD_CAL_RECUR, Value.CastInt32()); break; case SdRecurFreq: Value = GetObject()->SetInt(FIELD_CAL_RECUR_FREQ, Value.CastInt32()); break; case SdRecurInterval: Value = GetObject()->SetInt(FIELD_CAL_RECUR_INTERVAL, Value.CastInt32()); break; case SdRecurEndCount: Value = GetObject()->SetInt(FIELD_CAL_RECUR_END_COUNT, Value.CastInt32()); break; case SdRecurEndType: Value = GetObject()->SetInt(FIELD_CAL_RECUR_END_TYPE, Value.CastInt32()); break; case SdRecurFilterDays: Value = GetObject()->SetInt(FIELD_CAL_RECUR_FILTER_DAYS, Value.CastInt32()); break; case SdRecurFilterMonths: Value = GetObject()->SetInt(FIELD_CAL_RECUR_FILTER_MONTHS, Value.CastInt32()); break; case SdPrivacy: Value = GetObject()->SetInt(FIELD_CAL_PRIVACY, Value.CastInt32()); break; case SdColour: Value = GetObject()->SetInt(FIELD_COLOUR, Value.CastInt32()); break; case SdAllDay: Value = GetObject()->SetInt(FIELD_CAL_ALL_DAY, Value.CastInt32()); break; // Date time fields case SdStart: return SetDateField(FIELD_CAL_START_UTC, Value); case SdEnd: return SetDateField(FIELD_CAL_END_UTC, Value); case SdDateModified: return SetDateField(FIELD_DATE_MODIFIED, Value); default: return false; } return true; } bool Calendar::CallMethod(const char *MethodName, LVariant *ReturnValue, LArray &Args) { // ScribeDomType Fld = StrToDom(MethodName); return Thing::CallMethod(MethodName, ReturnValue, Args); } ////////////////////////////////////////////////////////////////////////////// bool SerializeUi(ItemFieldDef *Defs, LDataI *Object, LViewI *View, bool ToUi) { if (!Object || !Defs || !View) return false; if (ToUi) { for (ItemFieldDef *d = Defs; d->FieldId; d++) { if (d->CtrlId <= 0) continue; switch (d->Type) { case GV_STRING: { auto s = Object->GetStr(d->FieldId); View->SetCtrlName(d->CtrlId, s); break; } case GV_INT32: { int64 i = Object->GetInt(d->FieldId); View->SetCtrlValue(d->CtrlId, i); break; } case GV_DATETIME: { char s[64] = ""; auto dt = Object->GetDate(d->FieldId); if (dt && dt->Year()) dt->Get(s, sizeof(s)); View->SetCtrlName(d->CtrlId, s); break; } default: { LAssert(0); break; } } } } else { for (ItemFieldDef *d = Defs; d->FieldId; d++) { if (d->CtrlId <= 0) continue; switch (d->Type) { case GV_STRING: { const char *s = View->GetCtrlName(d->CtrlId); Object->SetStr(d->FieldId, s); break; } case GV_INT32: { int i = (int)View->GetCtrlValue(d->CtrlId); Object->SetInt(d->FieldId, i); break; } case GV_DATETIME: { const char *s = View->GetCtrlName(d->CtrlId); LDateTime dt; dt.Set(s); Object->SetDate(d->FieldId, &dt); break; } default: { LAssert(0); break; } } } } return true; } ////////////////////////////////////////////////////////////////////////////// class LEditDropDown : public LEdit { public: enum DropType { DropNone, DropDate, DropTime, }; protected: DropType Type; LAutoPtr Popup; public: LEditDropDown(int id) : LEdit(id, 0, 0, 60, 20, NULL) { Type = DropNone; SetObjectName(Res_Custom); } void SetType(DropType type) { Type = type; } const char *GetClass() { return "LEditDropDown"; } void OnMouseClick(LMouse &m) { LEdit::OnMouseClick(m); if (Focus() && Popup && !Popup->Visible()) { Popup->Visible(true); } } void OnFocus(bool f) { if (f) { if (!Popup) { if (Type == DropDate) Popup.Reset(new LDatePopup(this)); else if (Type == DropTime) Popup.Reset(new LTimePopup(this)); if (Popup) { Popup->TakeFocus(false); LPoint p(0, Y()); PointToScreen(p); LRect r = Popup->GetPos(); r.Offset(p.x - r.x1, p.y - r.y1); Popup->SetPos(r); } } if (Popup) Popup->Visible(true); } } int OnNotify(LViewI *Wnd, LNotification n) { /* LgiTrace("OnNotify %s, %i\n", Wnd->GetClass(), Flags); if (Wnd == (LViewI*)Popup) { GDatePopup *Date; if ((Date = dynamic_cast(Popup.Get()))) { LDateTime Ts = Date->Get(); char s[256]; Ts.GetDate(s, sizeof(s)); Name(s); } else LgiTrace("%s:%i - Incorrect pop up type.\n", _FL); } */ return 0; } void OnChildrenChanged(LViewI *Wnd, bool Attaching) { if (Wnd == (LViewI*)Popup.Get() && !Attaching) { // This gets called in the destructor of the Popup, so we should // lose the pointer to it. Popup.Release(); } } }; struct LEditDropDownFactory : public LViewFactory { LView *NewView(const char *Class, LRect *Pos, const char *Text) { if (!_stricmp(Class, "LEditDropDown")) { return new LEditDropDown(-1); } return NULL; } } EditDropDownFactory; ////////////////////////////////////////////////////////////////////////////// class LRecurDlg : public LDialog { CalendarUi *Ui; LEditDropDown *EndOnDate; LCombo *Repeats; bool AcceptNotify; public: LRecurDlg(CalendarUi *ui) { EndOnDate = NULL; Repeats = NULL; AcceptNotify = true; Ui = ui; SetParent(ui); if (LoadFromResource(IDD_CAL_RECUR)) { if (GetViewById(IDC_ON_DATE, EndOnDate)) { EndOnDate->SetType(LEditDropDown::DropDate); LDateTime dt; dt.SetNow(); char s[64]; dt.GetDate(s, sizeof(s)); EndOnDate->Name(s); } if (GetViewById(IDC_REPEATS, Repeats)) { Repeats->Insert(LLoadString(IDS_DAY)); Repeats->Insert(LLoadString(IDS_WEEK)); Repeats->Insert(LLoadString(IDS_MONTH)); Repeats->Insert(LLoadString(IDS_YEAR)); Repeats->Value(1); } Radio(IDC_NEVER); SetCtrlValue(IDC_EVERY, 1); Serialize(false); } } ~LRecurDlg() { } void Serialize(bool Write) { int DayCtrls[] = { IDC_SUNDAY, IDC_MONDAY, IDC_TUESDAY, IDC_WEDNESDAY, IDC_THURSDAY, IDC_FRIDAY, IDC_SATURDAY }; Calendar *c = Ui->GetCal(); LDataI *o = c->GetObject(); if (Write) { // Dlg -> Object int64 v = Repeats->Value(); o->SetInt(FIELD_CAL_RECUR_FREQ, v); v = GetCtrlValue(IDC_EVERY); o->SetInt(FIELD_CAL_RECUR_INTERVAL, v); int DayFilter = 0; for (int i=0; i<7; i++) { if (GetCtrlValue(DayCtrls[i])) DayFilter |= 1 << i; } o->SetInt(FIELD_CAL_RECUR_FILTER_DAYS, DayFilter); if (GetCtrlValue(IDC_NEVER)) { o->SetInt(FIELD_CAL_RECUR_END_TYPE, CalEndNever); } else if (GetCtrlValue(IDC_AFTER)) { o->SetInt(FIELD_CAL_RECUR_END_TYPE, CalEndOnCount); o->SetInt(FIELD_CAL_RECUR_END_COUNT, GetCtrlValue(IDC_AFTER_COUNT)); } else if (GetCtrlValue(IDC_ON)) { o->SetInt(FIELD_CAL_RECUR_END_TYPE, CalEndOnDate); LDateTime e; e.SetDate(GetCtrlName(IDC_ON_DATE)); e.SetTime("11:59:59"); o->SetDate(FIELD_CAL_RECUR_END_DATE, &e); } else LAssert(0); } else { // Object -> Dlg int64 v = o->GetInt(FIELD_CAL_RECUR_FREQ); Repeats->Value(v); OnRepeat(v); v = o->GetInt(FIELD_CAL_RECUR_INTERVAL); SetCtrlValue(IDC_EVERY, MAX(v, 1)); int DayFilter = (int)o->GetInt(FIELD_CAL_RECUR_FILTER_DAYS); for (int i=0; i<7; i++) { SetCtrlValue(DayCtrls[i], (DayFilter & (1 << i)) ? 1 : 0); } CalRecurEndType EndType = (CalRecurEndType) o->GetInt(FIELD_CAL_RECUR_END_TYPE); if (EndType == CalEndOnCount) { Radio(IDC_AFTER); SetCtrlValue(IDC_AFTER_COUNT, o->GetInt(FIELD_CAL_RECUR_END_COUNT)); } else if (EndType == CalEndOnDate) { Radio(IDC_ON); auto e = o->GetDate(FIELD_CAL_RECUR_END_DATE); if (e) SetCtrlName(IDC_ON_DATE, e->GetDate()); } else // Default to never... { Radio(IDC_NEVER); } } } void Radio(int Id) { AcceptNotify = false; SetCtrlValue(IDC_NEVER, Id == IDC_NEVER); SetCtrlValue(IDC_AFTER, Id == IDC_AFTER); SetCtrlEnabled(IDC_AFTER_COUNT, Id == IDC_AFTER); SetCtrlEnabled(IDC_OCCURRENCES, Id == IDC_AFTER); SetCtrlValue(IDC_ON, Id == IDC_ON); SetCtrlEnabled(IDC_ON_DATE, Id == IDC_ON); AcceptNotify = true; } void OnRepeat(int64 Val) { LViewI *v; if (!GetViewById(IDC_TIME_TYPE, v)) return; switch (Val) { case 0: v->Name(LLoadString(IDS_DAYS)); break; case 1: v->Name(LLoadString(IDS_WEEKS)); break; case 2: v->Name(LLoadString(IDS_MONTHS)); break; case 3: v->Name(LLoadString(IDS_YEARS)); break; } v->SendNotify(LNotifyTableLayoutRefresh); } int OnNotify(LViewI *Ctrl, LNotification n) { if (!AcceptNotify) return 0; switch (Ctrl->GetId()) { case IDC_NEVER: case IDC_AFTER: case IDC_ON: Radio(Ctrl->GetId()); break; case IDC_REPEATS: OnRepeat(Ctrl->Value()); break; case IDOK: Serialize(true); // Fall through case IDCANCEL: EndModal(Ctrl->GetId() == IDOK); break; } return 0; } }; /////////////////////////////////////////////////////////////////////////////// struct CalendarUiPriv { CalendarUi *Ui; LEditDropDown *StartDate, *StartTime; LEditDropDown *EndDate, *EndTime; LEdit *Entry; AddressBrowse *Browse; LList *Guests; LList *Reminders; LCombo *ReminderType; LCombo *ReminderUnit; LCombo *CalSelect; LColourSelect *Colour; CalendarUiPriv(CalendarUi *ui) { Ui = ui; StartDate = StartTime = NULL; EndDate = EndTime = NULL; CalSelect = NULL; Entry = NULL; Browse = NULL; Guests = NULL; Colour = NULL; Reminders = NULL; ReminderType = NULL; ReminderUnit = NULL; } void OnGuest() { const char *g = Ui->GetCtrlName(IDC_GUEST_ENTRY); if (!g) return; Mailto mt(Ui->App, g); for (auto a: mt.To) { ListAddr *la = new ListAddr(Ui->App, a); if (la) { Guests->Insert(la); } } Ui->SetCtrlName(IDC_GUEST_ENTRY, ""); } }; CalendarUi::CalendarUi(Calendar *item) : ThingUi(item, LLoadString(IDS_CAL_EVENT)) { NotifyOn = false; Item = item; d = new CalendarUiPriv(this); #if WINNATIVE CreateClassW32("Event", LoadIcon(LProcessInst(), MAKEINTRESOURCE(IDI_EVENT))); #endif if (!Attach(NULL)) LAssert(0); else { LoadFromResource(IDD_CAL, this); AttachChildren(); if (!SerializeState(Item->App->GetOptions(), OPT_CalendarEventPos, true)) { LRect p(100, 100, 900, 700); SetPos(p); MoveToCenter(); } if (GetViewById(IDC_START_DATE, d->StartDate)) { d->StartDate->SetType(LEditDropDown::DropDate); } if (GetViewById(IDC_START_TIME, d->StartTime)) { d->StartTime->SetType(LEditDropDown::DropTime); } if (GetViewById(IDC_END_DATE, d->EndDate)) { d->EndDate->SetType(LEditDropDown::DropDate); } if (GetViewById(IDC_END_TIME, d->EndTime)) { d->EndTime->SetType(LEditDropDown::DropTime); } GetViewById(IDC_GUEST_ENTRY, d->Entry); if (GetViewById(IDC_GUESTS, d->Guests)) { d->Guests->AddColumn(LLoadString(IDS_ADDRESS), 120); d->Guests->AddColumn(LLoadString(IDS_NAME), 120); d->Guests->ShowColumnHeader(false); } if (GetViewById(IDC_REMINDERS, d->Reminders)) { d->Reminders->AddColumn(LLoadString(FIELD_CAL_REMINDER_TIME), 200); d->Reminders->AddColumn(LLoadString(IDC_DELETE), 20); d->Reminders->ShowColumnHeader(false); } if (GetViewById(IDC_REMINDER_TYPE, d->ReminderType)) { d->ReminderType->Insert(LLoadString(IDS_EMAIL)); d->ReminderType->Insert(LLoadString(IDS_POPUP)); d->ReminderType->Insert("Script"); d->ReminderType->Value(1); } SetCtrlValue(IDC_REMINDER_VALUE, 10); if (GetViewById(IDC_REMINDER_UNIT, d->ReminderUnit)) { d->ReminderUnit->Insert(LLoadString(IDS_MINUTES)); d->ReminderUnit->Insert(LLoadString(IDS_HOURS)); d->ReminderUnit->Insert(LLoadString(IDS_DAYS)); d->ReminderUnit->Insert(LLoadString(IDS_WEEKS)); d->ReminderUnit->Value(0); } SetCtrlValue(IDC_AVAILABLE_TYPE, 1); if (GetViewById(IDC_COLOUR, d->Colour)) { LArray Colours; Colours.Add(LColour(84, 132, 237, 255)); Colours.Add(LColour(164, 189, 252, 255)); Colours.Add(LColour(70, 214, 219, 255)); Colours.Add(LColour(122, 231, 191, 255)); Colours.Add(LColour(81, 183, 73, 255)); Colours.Add(LColour(251, 215, 91, 255)); Colours.Add(LColour(255, 184, 120, 255)); Colours.Add(LColour(255, 136, 124, 255)); Colours.Add(LColour(220, 33, 39, 255)); Colours.Add(LColour(219, 173, 255, 255)); Colours.Add(LColour(225, 225, 225, 255)); d->Colour->SetColourList(&Colours); d->Colour->Value(0); } if (GetViewById(IDC_CALENDAR, d->CalSelect)) { LArray Sources; if (Item->App->GetCalendarSources(Sources)) { for (unsigned i=0; iCalSelect->Insert(cs->GetName()); } } } LView *s; if (GetViewById(IDC_SUBJECT, s)) s->Focus(true); LButton *btn; if (GetViewById(IDC_SAVE, btn)) btn->Default(true); OnLoad(); Visible(true); NotifyOn = true; } LViewI *v; if (GetViewById(IDC_REMINDER_TYPE, v)) { LNotification note; OnNotify(v, note); } RegisterHook(this, LKeyEvents); } CalendarUi::~CalendarUi() { if (Item) { if (Item->App) SerializeState(Item->App->GetOptions(), OPT_CalendarEventPos, false); Item->SetUI(NULL); } } bool CalendarUi::OnViewKey(LView *v, LKey &k) { if (k.CtrlCmd()) { switch (k.c16) { case 's': case 'S': { if (k.Down()) OnSave(); return true; } case 'w': case 'W': { if (k.Down()) { OnSave(); Quit(); } return true; } } if (k.vkey == LK_RETURN) { if (!k.Down()) { OnSave(); Quit(); } return true; } } return false; } int CalendarUi::OnCommand(int Cmd, int Event, OsView Window) { return 0; } void CalendarUi::OnPosChange() { LWindow::OnPosChange(); } void CalendarUi::CheckConsistancy() { auto St = CurrentStart(); auto En = CurrentEnd(); if (En < St) { En = St; En.AddMinutes(30); SetCtrlName(IDC_END_DATE, En.GetDate().Get()); SetCtrlName(IDC_END_TIME, En.GetTime().Get()); } } LDateTime CalendarUi::CurrentStart() { LDateTime dt; dt.SetDate(GetCtrlName(IDC_START_DATE)); dt.SetTime(GetCtrlName(IDC_START_TIME)); return dt; } LDateTime CalendarUi::CurrentEnd() { LDateTime dt; dt.SetDate(GetCtrlName(IDC_END_DATE)); dt.SetTime(GetCtrlName(IDC_END_TIME)); return dt; } void CalendarUi::UpdateStartRelative() { LDateTime dt = CurrentStart(); if (dt.IsValid()) { const char *s = RelativeTime(dt); SetCtrlName(IDC_START_REL, s); } } void CalendarUi::UpdateEndRelative() { LDateTime dt = CurrentEnd(); if (dt.IsValid()) { const char *s = RelativeTime(dt); SetCtrlName(IDC_END_REL, s); } } void CalendarUi::UpdateRelative() { CheckConsistancy(); UpdateStartRelative(); UpdateEndRelative(); } int CalendarUi::OnNotify(LViewI *Ctrl, LNotification n) { if (!NotifyOn) return false; switch (Ctrl->GetId()) { case IDC_START_DATE: case IDC_START_TIME: { CheckConsistancy(); UpdateStartRelative(); break; } case IDC_END_DATE: case IDC_END_TIME: { CheckConsistancy(); UpdateEndRelative(); break; } case IDC_REMINDER_TYPE: { LViewI *Label, *Param; if (GetViewById(IDC_PARAM_LABEL, Label) && GetViewById(IDC_REMINDER_PARAM, Param)) { Param->Name(NULL); switch (Ctrl->Value()) { case CalEmail: Label->Name("optional email:"); Param->Enabled(true); break; default: case CalPopup: Label->Name(NULL); Param->Enabled(false); break; case CalScriptCallback: Label->Name("callback fn:"); Param->Enabled(true); break; } } break; } case IDC_ALL_DAY: { int64 AllDay = Ctrl->Value(); LDataI *o = Item->GetObject(); char s[256] = ""; auto dt = o->GetDate(FIELD_CAL_START_UTC); if (dt) { LDateTime tmp = *dt; tmp.ToLocal(true); tmp.GetTime(s, sizeof(s)); SetCtrlName(IDC_START_TIME, AllDay ? "" : s); SetCtrlEnabled(IDC_START_TIME, !AllDay); } dt = o->GetDate(FIELD_CAL_END_UTC); if (dt) { LDateTime tmp = *dt; tmp.ToLocal(true); tmp.GetTime(s, sizeof(s)); SetCtrlName(IDC_END_TIME, AllDay ? "" : s); SetCtrlEnabled(IDC_END_TIME, !AllDay); } break; } case IDC_SUBJECT: { SetCtrlEnabled(IDC_SAVE, ValidStr(Ctrl->Name())); break; } case IDC_SHOW_LOCATION: { LString Loc = GetCtrlName(IDC_LOCATION); if (ValidStr(Loc)) { for (char *c = Loc; *c; c++) { if (Strchr(WhiteSpace, *c)) *c = '+'; } LString Uri; Uri.Printf("http://maps.google.com/?q=%s", Loc.Get()); LExecute(Uri); } break; } case IDC_GUESTS: { if (n.Type == LNotifyItemInsert) { d->Guests->ResizeColumnsToContent(); } else if (n.Type == LNotifyDeleteKey) { List la; d->Guests->GetSelection(la); la.DeleteObjects(); } break; } case IDC_GUEST_ENTRY: { if (d->Entry) { if (ValidStr(d->Entry->Name())) { if (!d->Browse) { d->Browse = new AddressBrowse(Item->App, d->Entry, d->Guests, NULL); } } if (d->Browse) { d->Browse->OnNotify(d->Entry, n); } if (n.Type == LNotifyReturnKey) { d->OnGuest(); } } break; } case IDC_REPEAT: { if (!Ctrl->Value()) break; auto Dlg = new LRecurDlg(this); Dlg->DoModal([this, Dlg](auto dlg, auto ctrlId) { if (ctrlId) SetCtrlValue(IDC_REPEAT, 0); delete dlg; }); break; } case IDC_TIMEZONE: { auto Tz = Item->GetObject()->GetStr(FIELD_CAL_TIMEZONE); auto Dlg = new LInput(this, Tz, "Time zone:", "Calendar Event Timezone"); Dlg->DoModal([this, Dlg](auto dlg, auto Result) { if (Result) { Item->GetObject()->SetStr(FIELD_CAL_TIMEZONE, Dlg->GetStr()); Item->SetDirty(); } delete dlg; }); break; } case IDC_REMINDER_ADD: { const char *v = GetCtrlName(IDC_REMINDER_VALUE); const char *param = GetCtrlName(IDC_REMINDER_PARAM); d->Reminders->Insert ( new ReminderItem ( (CalendarReminderType) d->ReminderType->Value(), v ? (float)atof(v) : 1.0f, (CalendarReminderUnits) d->ReminderUnit->Value(), param ) ); d->Reminders->ResizeColumnsToContent(); break; } case IDC_ADD_GUEST: { d->OnGuest(); break; } case IDC_SAVE: { LViewI *v; if (GetViewById(IDC_GUEST_ENTRY, v) && v->Focus()) { d->OnGuest(); break; } else { OnSave(); // Fall through } } case IDCANCEL: { Quit(); break; } } return 0; } void CalendarUi::OnLoad() { // Sanity check if (!Item) { LAssert(!"No item."); return; } LDataI *o = Item->GetObject(); if (!o) { LAssert(!"No object."); return; } // Copy object values into UI int64 AllDay = false; auto CalSubject = o->GetStr(FIELD_CAL_SUBJECT); SetCtrlName(IDC_SUBJECT, CalSubject); SetCtrlName(IDC_LOCATION, o->GetStr(FIELD_CAL_LOCATION)); SetCtrlName(IDC_DESCRIPTION, o->GetStr(FIELD_CAL_NOTES)); SetCtrlValue(IDC_ALL_DAY, AllDay = o->GetInt(FIELD_CAL_ALL_DAY)); if (d->Guests) { LJson j(o->GetStr(FIELD_ATTENDEE_JSON)); for (auto g: j.GetArray(NULL)) { LAutoPtr la(new ListAddr(App)); if (la) { la->sAddr = g.Get("email"); la->sName = g.Get("name"); d->Guests->Insert(la.Release()); } } d->Guests->ResizeColumnsToContent(); } if (d->Reminders) { d->Reminders->Empty(); LString Rem = o->GetStr(FIELD_CAL_REMINDERS); LString::Array a = Rem.SplitDelimit("\n"); for (unsigned i=0; iSetString(a[i])) d->Reminders->Insert(ri); else delete ri; } } d->Reminders->ResizeColumnsToContent(); } CalendarShowTimeAs Show = (CalendarShowTimeAs)o->GetInt(FIELD_CAL_SHOW_TIME_AS); switch (Show) { case CalFree: SetCtrlValue(IDC_AVAILABLE_TYPE, 0); break; case CalTentative: SetCtrlValue(IDC_AVAILABLE_TYPE, 1); break; default: case CalBusy: SetCtrlValue(IDC_AVAILABLE_TYPE, 2); break; } CalendarPrivacyType Priv = (CalendarPrivacyType)o->GetInt(FIELD_CAL_PRIVACY); switch (Priv) { default: SetCtrlValue(IDC_PRIVACY_TYPE, 0); break; case CalPublic: SetCtrlValue(IDC_PRIVACY_TYPE, 1); break; case CalPrivate: SetCtrlValue(IDC_PRIVACY_TYPE, 2); break; } int64 Col = o->GetInt(FIELD_COLOUR); if (Col >= 0) d->Colour->Value(Col); for (unsigned i=0; iGetName(); LString i_path; if (Item->GetFolder()) i_path = Item->GetFolder()->GetPath(); if (s_path && i_path && _stricmp(s_path, i_path) == 0) { SetCtrlValue(IDC_CALENDAR, i); break; } } char s[256] = ""; auto dt = o->GetDate(FIELD_CAL_START_UTC); if (dt) { #if DEBUG_DATES printf("Load.Start.UTC=%s\n", dt->Get().Get()); #endif LDateTime tmp = *dt; tmp.SetTimeZone(0, false); tmp.ToLocal(true); #if DEBUG_DATES printf("Load.Start.Local=%s\n", tmp.Get().Get()); #endif tmp.GetDate(s, sizeof(s)); SetCtrlName(IDC_START_DATE, s); tmp.GetTime(s, sizeof(s)); SetCtrlName(IDC_START_TIME, AllDay ? "" : s); SetCtrlEnabled(IDC_START_TIME, !AllDay); } dt = o->GetDate(FIELD_CAL_END_UTC); if (dt) { #if DEBUG_DATES printf("Load.End.UTC=%s\n", dt->Get().Get()); #endif LDateTime tmp = *dt; tmp.SetTimeZone(0, false); tmp.ToLocal(true); #if DEBUG_DATES printf("Load.End.Local=%s\n", tmp.Get().Get()); #endif tmp.GetDate(s, sizeof(s)); SetCtrlName(IDC_END_DATE, s); tmp.GetTime(s, sizeof(s)); SetCtrlName(IDC_END_TIME, AllDay ? "" : s); SetCtrlEnabled(IDC_END_TIME, !AllDay); } SetCtrlValue(IDC_REPEAT, o->GetInt(FIELD_CAL_RECUR)); LViewI *ctrl; if (GetViewById(IDC_SUBJECT, ctrl)) { LNotification note(LNotifyValueChanged); OnNotify(ctrl, note); } if (ValidStr(CalSubject)) { LString s; s.Printf("%s - %s", LLoadString(IDS_CAL_EVENT), CalSubject); Name(s); } UpdateRelative(); } void CalendarUi::OnSave() { // Sanity check if (!Item) { LAssert(!"No item."); return; } LDataI *o = Item->GetObject(); if (!o) { LAssert(!"No object."); return; } // Save UI values into object bool AllDay = false; o->SetStr(FIELD_CAL_SUBJECT, GetCtrlName(IDC_SUBJECT)); o->SetStr(FIELD_CAL_LOCATION, GetCtrlName(IDC_LOCATION)); o->SetStr(FIELD_CAL_NOTES, GetCtrlName(IDC_DESCRIPTION)); o->SetInt(FIELD_CAL_ALL_DAY, AllDay = (GetCtrlValue(IDC_ALL_DAY) != 0)); if (d->Guests) { List All; if (d->Guests->GetAll(All)) { LString::Array a; LString Sep(", "); for (auto la: All) { LString e; if (la->Serialize(e, true)) a.Add(e); } LString Guests = Sep.Join(a); o->SetStr(FIELD_TO, Guests); } } if (d->Reminders) { List All; if (d->Reminders->GetAll(All)) { LString::Array a; LString Sep("\n"); for (auto ri: All) { a.Add(ri->GetString()); } LString Reminders = Sep.Join(a); o->SetStr(FIELD_CAL_REMINDERS, Reminders); } else { o->SetStr(FIELD_CAL_REMINDERS, ""); } } int64 Show = GetCtrlValue(IDC_AVAILABLE_TYPE); switch (Show) { case 0: o->SetInt(FIELD_CAL_SHOW_TIME_AS, CalFree); break; case 1: o->SetInt(FIELD_CAL_SHOW_TIME_AS, CalTentative); break; default: case 2: o->SetInt(FIELD_CAL_SHOW_TIME_AS, CalBusy); break; } int64 Priv = GetCtrlValue(IDC_PRIVACY_TYPE); switch (Priv) { default: case 0: o->SetInt(FIELD_CAL_PRIVACY, CalDefaultPriv); break; case 1: o->SetInt(FIELD_CAL_PRIVACY, CalPublic); break; case 2: o->SetInt(FIELD_CAL_PRIVACY, CalPrivate); break; } auto Col = d->Colour->Value(); o->SetInt(FIELD_COLOUR, Col > 0 ? Col : -1); /* FIXME: change calendar item location if the user selects a different cal for (unsigned i=0; iSources.Length(); i++) { CalendarSource *src = d->Sources[i]; LAutoString s_path(src->GetPath()); LAutoString i_path; if (Item->GetFolder()) i_path = Item->GetFolder()->GetPath(); if (s_path && i_path && _stricmp(s_path, i_path) == 0) { SetCtrlValue(IDC_CALENDAR, i); break; } } */ LDateTime dt; dt.SetDate(GetCtrlName(IDC_START_DATE)); dt.SetTime(AllDay ? "0:0:0" : GetCtrlName(IDC_START_TIME)); #if DEBUG_DATES printf("Start.Local=%s\n", dt.Get().Get()); #endif dt.ToUtc(true); #if DEBUG_DATES printf("Start.UTC=%s\n", dt.Get().Get()); #endif o->SetDate(FIELD_CAL_START_UTC, &dt); dt.Empty(); dt.SetDate(GetCtrlName(IDC_END_DATE)); dt.SetTime(AllDay ? "11:59:59" : GetCtrlName(IDC_END_TIME)); #if DEBUG_DATES printf("End.Local=%s\n", dt.Get().Get()); #endif dt.ToUtc(true); #if DEBUG_DATES printf("End.UTC=%s\n", dt.Get().Get()); #endif o->SetDate(FIELD_CAL_END_UTC, &dt); o->SetInt(FIELD_CAL_RECUR, GetCtrlValue(IDC_REPEAT)); Item->Update(); Item->Save(); } ////////////////////////////////////////////////////////////// int LDateTimeViewBase = 1000; class LDateTimeView : public LLayout, public ResObject { LEdit *Edit; LDateDropDown *Date; LTimeDropDown *Time; public: LDateTimeView() : ResObject(Res_Custom) { AddView(Edit = new LEdit(10, 0, 0, 60, 20, NULL)); AddView(Date = new LDateDropDown()); AddView(Time = new LTimeDropDown()); Edit->SetId(LDateTimeViewBase++); Date->SetNotify(Edit); Date->SetId(LDateTimeViewBase++); Time->SetNotify(Edit); Time->SetId(LDateTimeViewBase++); } bool OnLayout(LViewLayoutInfo &Inf) { if (!Inf.Width.Max) { Inf.Width.Max = 220; Inf.Width.Min = 150; } else if (!Inf.Height.Max) { Inf.Height.Max = Inf.Height.Min = LSysFont->GetHeight() + 6; } else return false; return true; } void OnCreate() { AttachChildren(); } void OnPosChange() { LRect c = GetClient(); // int cy = c.Y(); // int py = Y(); LRect tc = c; tc.x1 = tc.x2 - 20; LRect dc = c; dc.x2 = tc.x1 - 1; dc.x1 = dc.x2 - 20; Date->SetPos(dc); Time->SetPos(tc); c.x2 = dc.x1 - 1; Edit->SetPos(c); } const char *Name() { return Edit->Name(); } bool Name(const char *s) { return Edit->Name(s); } }; class LDateTimeViewFactory : public LViewFactory { LView *NewView(const char *Class, LRect *Pos, const char *Text) { if (!_stricmp(Class, "LDateTimeView")) return new LDateTimeView; return NULL; } public: } DateTimeViewFactory; diff --git a/Code/Components.cpp b/Code/Components.cpp --- a/Code/Components.cpp +++ b/Code/Components.cpp @@ -1,786 +1,786 @@ /* Missing capabilities and their installation: 1) An instance of a LCapabilityClient will receive a call to it's NeedsCapability function. e.g. The parent class of ScribeAccount (LCapabilityClient) calls into the ScribeWnd::NeedsCapability (a virtual function of the parent LCapabilityTarget class). 2) ScribeWnd::NeedsCapability puts up a MissingCapsBar to show the user what is happening. 3) The user clicks one of the action buttons and the MissingCapsBar calls CapabilityInstaller::StartAction which is implemented in ScribeWnd::StartAction. 4) If the action is to install some component the CapabilityInstaller::StartAction method is called to begin the process of querying the memecode site for suitable downloads and then downloading the files. 5) That function creates a CapabilityInstallerPriv::InstallJob instance with the capability to install. This is run in a worker thread. 6) CapabilityInstallerPriv::Main loops through all the jobs, queries the memecode site for downloads, and then initiates the HTTP downloads to a temporary folder. 7) If the output folder is writable it just moves the files straight to the destination folder. Otherwise it will call the Updater.exe to move the files, which will request admin permissions as part of it's manifest. 8) Finally an M_UPDATE message is posted back to the MissingCapsBar::OnEvent method which sets the final message and then creates a timer to hide itself. 9) After the timer expires the LCapabilityTarget::OnCloseInstaller (implemented in ScribeWnd::OnCloseInstaller) is called to delete the MissingCapsBar. */ #include "lgi/common/Lgi.h" #include "Scribe.h" #include "Components.h" #include "lgi/common/Button.h" #include "lgi/common/Http.h" #include "lgi/common/DisplayString.h" #define MISSING_CAPS_BAR_COUNTDOWN 4 // seconds #define MISSING_ACTION_BASE 100 struct MissingCapsBarPriv { LColour Back; int CountDown; - LToken Msg; + LString::Array Msg; LArray Strs; MissingCapsBarPriv() : Back(0xd2, 0x40, 0x40) { CountDown = -1; } ~MissingCapsBarPriv() { Strs.DeleteObjects(); } void SetMsg(const char *m) { Msg.Empty(); - Msg.Parse(m, "\n"); + Msg = LString(m).SplitDelimit("\n"); Strs.DeleteObjects(); } }; MissingCapsBar::MissingCapsBar( LCapabilityTarget *owner, LCapabilityTarget::CapsHash *a, const char *msg, CapabilityInstaller *inst, LArray &actions, LColour *background) { d = new MissingCapsBarPriv(); Owner = owner; Installer = inst; Progress = NULL; Caps = a; ProgCtrl = NULL; if (background) d->Back = *background; d->SetMsg(msg); int ContentY = (int)d->Msg.Length() * LSysFont->GetHeight(); for (unsigned i=0; iBack.c32()); b->GetCss(true)->NoPaintColor(Bk); ContentY = MAX(ContentY, b->Y()); if (i == actions.Length() - 1) b->Default(true); Btns.Add(b); AddView(b); } } LRect r(0, 0, 200, ContentY + 8); SetPos(r); } MissingCapsBar::~MissingCapsBar() { if (Progress) { if (Progress->Lock(_FL)) { Progress->Ui = NULL; Progress->Unlock(); Progress->DecRef(); } else LAssert(0); Progress = NULL; } DeleteObj(d); } void MissingCapsBar::SetMsg(const char *m) { d->SetMsg(m); Invalidate(); } void MissingCapsBar::OnCreate() { AttachChildren(); SetPulse(1000); } void MissingCapsBar::OnPosChange() { LRect b = GetClient(); int x = b.x2 - 4; for (ssize_t i=Btns.Length()-1; i>=0; i--) { LButton *Btn = Btns[i]; LRect r = Btn->GetPos(); r.x2 = x; r.x1 = x - Btn->X() + 1; x = r.x1 - 4; int Px = (b.Y() - Btn->Y()) >> 1; r.y1 = Px; r.y2 = Px + Btn->Y() - 1; Btn->SetPos(r); } if (ProgCtrl) { LRect r = ProgCtrl->GetPos(); r.Offset((x - r.X()) - r.x1, ((b.Y() - r.Y()) / 2) - r.y1); ProgCtrl->SetPos(r); } } void MissingCapsBar::OnPaint(LSurface *pDC) { LRect c = GetClient(); pDC->Colour(d->Back); pDC->Rectangle(); if (d->Msg.Length()) { if (!d->Strs.Length()) { for (unsigned i=0; iMsg.Length(); i++) { d->Strs.Add(new LDisplayString(LSysFont, d->Msg[i])); } } LSysFont->Transparent(true); LSysFont->Colour(LColour(255, 255, 255), d->Back); int Px = Y() - (LSysFont->GetHeight() * (int)d->Msg.Length()); Px >>= 1; for (unsigned i=0; iStrs.Length(); i++) { d->Strs[i]->Draw(pDC, 10, Px + (i * LSysFont->GetHeight()), &c); } } } bool MissingCapsBar::Pour(LRegion &r) { LRect *p = FindLargest(r); if (!p) return false; LRect rc = *p; rc.y2 = rc.y1 + Y() - 1; SetPos(rc); return true; } int MissingCapsBar::OnNotify(LViewI *c, LNotification n) { if (c->GetId() >= MISSING_ACTION_BASE) { int Idx = c->GetId() - MISSING_ACTION_BASE; #if DEBUG_CAPABILITIES LgiTrace("%s:%i - Idx=%i/%i\n", _FL, Idx, Btns.Length()); #endif if (Idx < (int)Btns.Length()) { c = Btns[Idx]; const char *Action = c->Name(); if ((Progress = Installer->StartAction(this, Caps, Action))) { #if DEBUG_CAPABILITIES LgiTrace("%s:%i - StartAction=%p\n", _FL, Idx, Progress); #endif c->Enabled(false); } else { #if DEBUG_CAPABILITIES LgiTrace("%s:%i - StartAction failed\n", _FL, Idx, Progress); #endif Detach(); Owner->OnCloseInstaller(); delete this; } } } return 0; } void MissingCapsBar::OnPulse() { if (Progress) { bool HasProg = Progress->TotalSize > 0 && !Progress->Finished; bool HasCtrl = ProgCtrl != NULL; if (HasProg ^ HasCtrl) { if (HasProg) { // Create ctrl ProgCtrl = new LProgressView(-1, 0, 0, 150, 12, NULL); ProgCtrl->SetRange(Progress->TotalSize); ProgCtrl->Attach(this); } else { DeleteObj(ProgCtrl); } OnPosChange(); } if (ProgCtrl) { ProgCtrl->Value(Progress->CurrentPos); } } if (d->CountDown > 0) { d->CountDown--; if (d->CountDown <= 0) { // This detach means the LWindow::Pour won't alloc space for the MissingCapsBar Detach(); // This will remove the ownering windows ptr to us and repour the window. Owner->OnCloseInstaller(); // We can now RIP delete this; } } } LMessage::Param MissingCapsBar::OnEvent(LMessage *m) { switch (m->Msg()) { case M_UPDATE: { if (Progress && Progress->Lock(_FL)) { bool Finished = Progress->Finished; bool HasError = Progress->HasError; if (Progress->Msg) d->SetMsg(Progress->Msg); Progress->Msg.Reset(); Progress->Unlock(); Invalidate(); if (Finished) { // Is the case that the caller doesn't delete us then setup // a count down to automatically remove the install bar. d->CountDown = MISSING_CAPS_BAR_COUNTDOWN; if (ProgCtrl) ProgCtrl->Value(Progress->TotalSize); if (Progress->Lock(_FL)) { Progress->Ui = NULL; Progress->Unlock(); } Progress->DecRef(); Progress = NULL; // Tell the owner Owner->OnInstall(Caps, !HasError); // We _may_ get deleted here... so we can't call our parents "OnEvent" return 0; } } break; } } return LLayout::OnEvent(m); } class ProgressSocket : public LSocket { int64 *Prog; public: ProgressSocket(int64 *p) { Prog = p; } ssize_t Read( void *Data, ssize_t Len, int Flags) { ssize_t r = LSocket::Read(Data, Len, Flags); if (Prog && r > 0) *Prog += r; return r; } }; /////////////////////////////////////////////////////////////////////////////////// class CapabilityInstallerPriv : public LMutex, public LThread { public: struct InstallJob { InstallProgress *Prog; LAutoString Component; }; private: LAutoString TempFolder; LArray Jobs; bool Loop; public: LAutoString Uri, Proxy, App, Version; CapabilityInstallerPriv(const char *TmpFolder) : LMutex("ComponentInstaller.Mutex"), LThread("ComponentInstaller.Thread") { TempFolder.Reset(NewStr(TmpFolder)); Loop = true; } ~CapabilityInstallerPriv() { Loop = false; while (!IsExited()) LSleep(1); Jobs.DeleteObjects(); } void Msg(InstallProgress *Prog, const char *Fmt, ...) { char buffer[512]; va_list arg; va_start(arg, Fmt); vsprintf_s(buffer, sizeof(buffer), Fmt, arg); va_end(arg); LgiTrace("CapabilityInstaller: %s\n", buffer); if (Prog->Lock(_FL)) { Prog->Msg.Reset(NewStr(buffer)); if (Prog->Ui) Prog->Ui->PostEvent(M_UPDATE); Prog->Unlock(); } } void AddJob(InstallJob *j) { if (Lock(_FL)) { Jobs.Add(j); Unlock(); } } int Main() { while (Loop) { InstallJob *j = NULL; if (Lock(_FL)) { if (Jobs.Length()) { j = Jobs[0]; Jobs.DeleteAt(0, true); } Unlock(); } if (j) { bool Success = false; char OutputPath[MAX_PATH_LEN]; #ifdef MAC LMakePath(OutputPath, sizeof(OutputPath), LGetExeFile(), "Contents/Frameworks"); #else strcpy_s(OutputPath, sizeof(OutputPath), LGetExePath()); #endif Msg(j->Prog, "Getting download site..."); LHttp Http; if (Proxy) { LUri u(Proxy); Http.SetProxy(u.sHost, u.Port?u.Port:HTTP_PORT); } char Url[256]; const char *Os = LGetOsName(); #ifdef _MSC_VER int ch = #endif sprintf_s(Url, sizeof(Url), "%s?os=%s&wordsize=%i&app=%s&version=%s&component=%s", Uri.Get(), Os, (int)(sizeof(NativeInt)<<3), App.Get(), Version.Get(), j->Component.Get()); #ifdef _MSC_VER #if _MSC_VER == _MSC_VER_VS2008 ch += sprintf_s(Url+ch, sizeof(Url)-ch, "&tags=vc9"); #elif _MSC_VER == _MSC_VER_VS2013 ch += sprintf_s(Url+ch, sizeof(Url)-ch, "&tags=vc12"); #elif _MSC_VER == _MSC_VER_VS2015 ch += sprintf_s(Url+ch, sizeof(Url)-ch, "&tags=vc14"); #elif _MSC_VER == _MSC_VER_VS2017 ch += sprintf_s(Url+ch, sizeof(Url)-ch, "&tags=vc15"); #elif _MSC_VER >= _MSC_VER_VS2019 ch += sprintf_s(Url+ch, sizeof(Url)-ch, "&tags=vc16"); #else #error "Impl me." #endif #endif #if DEBUG_CAPABILITIES LgiTrace("%s:%i - ComponentURL: %s\n", _FL, Url); #endif LStringPipe Xml; int Status = 0; LUri u(Url); LAutoPtr Sock(new LSocket); Sock->SetTimeout(15 * 1000); if (!Http.Open(Sock, u.sHost, u.Port?u.Port:HTTP_PORT)) Msg(j->Prog, "Error: connection to %s failed.", u.sHost.Get()); else { if (!Http.Get(Url, NULL, &Status, &Xml, NULL)) Msg(j->Prog, "Error: HTTP get failed."); else { if (Status != 200) Msg(j->Prog, "Error: HTTP status %i", Status); else { LXmlTree t; LXmlTag r; if (!t.Read(&r, &Xml)) Msg(j->Prog, "Error: XML parsing error."); else { if (!r.IsTag("components")) Msg(j->Prog, "Error: Unexpected XML."); else { // Collect list of files to download LArray InFiles, TmpFiles; int64 TotalBytes = 0; for (auto c: r.Children) { if (c->IsTag("file") && c->GetContent()) { InFiles.Add(c->GetContent()); int Size = c->GetAsInt("size"); if (Size > 0) TotalBytes += Size; } } if (InFiles.Length() == 0) { LString s; s.Printf("Error: No downloads available (%s)", Url); Msg(j->Prog, s); } else { j->Prog->CurrentPos = 0; j->Prog->TotalSize = TotalBytes; Msg(j->Prog, "Downloading..."); for (unsigned i=0; iProg, "Error: No filename in download URL."); break; } File++; char OutPath[MAX_PATH_LEN]; LMakePath(OutPath, sizeof(OutPath), TempFolder, File); if (!Out.Open(OutPath, O_WRITE)) { Msg(j->Prog, "Error: Can't open '%s' for writing.", OutPath); break; } Sock.Reset(new ProgressSocket(&j->Prog->CurrentPos)); if (!Http.Open(Sock, u.sHost, u.Port?u.Port:HTTP_PORT)) { Msg(j->Prog, "Error: Can't connect to '%s'.", u.sHost.Get()); break; } if (!Http.Get(InFiles[i], NULL, &Status, &Out, NULL)) { Msg(j->Prog, "Error: HTTP get failed."); break; } if (Status == 200) { TmpFiles.Add(NewStr(OutPath)); } else { Msg(j->Prog, "Error: Download component '%s' failed with HTTP %i.", InFiles[i], Status); Out.Close(); FileDev->Delete(OutPath, false); } } if (TmpFiles.Length() == InFiles.Length()) { // Check to see if the install folder is writable... char p[MAX_PATH_LEN]; LMakePath(p, sizeof(p), OutputPath, "write_test.txt"); LFile f; bool Writable = f.Open(p, O_WRITE) != 0; if (Writable) { f.Close(); FileDev->Delete(p, false); } if (Writable) { // We can move them into place ourself... int Copied = 0; for (unsigned i=0; iCopy(t, p, &CopyStatus)) { Copied++; FileDev->Delete(t, false); } else { Msg(j->Prog, "Error: File copy failed."); LgiTrace("%s:%i - Copy '%s' failed during install component: %i (%s)\n", _FL, l, CopyStatus.GetCode(), CopyStatus.GetMsg().Get()); } } } if (Copied == TmpFiles.Length()) { Msg(j->Prog, "Component download completed."); Success = true; } } else { // Use privledge escalation to install the files... #ifdef WIN32 char Updater[MAX_PATH_LEN]; LMakePath(Updater, sizeof(Updater), OutputPath, "Updater.exe"); if (LFileExists(Updater)) { LStringPipe p; for (unsigned i=0; iProg, "Component download completed."); Success = true; } else Msg(j->Prog, "Error: Updater component returned an error."); } else { Msg(j->Prog, "Error: Failed to run the updater component."); } } else { Msg(j->Prog, "Error: Updater '%s' missing.", Updater); } #else LAssert(!"Impl privledge escalation for installing software."); #endif } if (Success && LFontSystem::Inst()) LFontSystem::Inst()->ResetLibCheck(); } } } } } } } #if defined(_DEBUG) && !DEBUG_CAPABILITIES if (!Success) LgiTrace("%s:%i - ComponentURL: %s\n", _FL, Url); #endif if (j->Prog->Lock(_FL)) { j->Prog->Finished = true; j->Prog->HasError = !Success; if (j->Prog->Ui) j->Prog->Ui->PostEvent(M_UPDATE); j->Prog->Unlock(); } j->Prog->DecRef(); DeleteObj(j); } else LSleep(200); } return 0; } }; CapabilityInstaller::CapabilityInstaller(const char *App, const char *Version, const char *Uri, const char *TmpPath, const char *Proxy) { d = new CapabilityInstallerPriv(TmpPath); d->App.Reset(NewStr(App)); d->Version.Reset(NewStr(Version)); d->Proxy.Reset(NewStr(Proxy)); d->Uri.Reset(NewStr(Uri)); d->Run(); } CapabilityInstaller::~CapabilityInstaller() { DeleteObj(d); } InstallProgress *CapabilityInstaller::StartAction(MissingCapsBar *Bar, LCapabilityTarget::CapsHash *Components, const char *Action) { if (!Components || !Action) { LgiTrace("%s:%i - CapabilityInstaller::StartAction invalid params\n", _FL); return NULL; } InstallProgress *p = NULL; if (d->Lock(_FL)) { if (!d->Proxy) d->Proxy = GetHttpProxy(); p = new InstallProgress; if (p) { p->IncRef(); // for the UI view... p->Ui = Bar; for (auto k : *Components) { CapabilityInstallerPriv::InstallJob *j = new CapabilityInstallerPriv::InstallJob; j->Prog = p; p->IncRef(); // The job owns a reference... j->Component.Reset(NewStr(k.key)); #if DEBUG_CAPABILITIES LgiTrace("%s:%i - AddJob(%s)\n", _FL, k); #endif d->AddJob(j); } } else { LgiTrace("%s:%i - Alloc failed.\n", _FL); } d->Unlock(); } else { LgiTrace("%s:%i - Couldn't lock.\n", _FL); } return p; } diff --git a/Code/ImpExp_Outlook.cpp b/Code/ImpExp_Outlook.cpp --- a/Code/ImpExp_Outlook.cpp +++ b/Code/ImpExp_Outlook.cpp @@ -1,3939 +1,3935 @@ #undef UNICODE #include "Scribe.h" #include "lgi/common/Com.h" #include "mapix.h" #include "mapiutil.h" #include "lgi/common/Button.h" #include "lgi/common/Combo.h" #include "lgi/common/Edit.h" #include "lgi/common/ProgressDlg.h" #include "lgi/common/RtfHtml.h" #include "Calendar.h" #include "resdefs.h" #include "lgi/common/LgiRes.h" #if _MSC_VER >= 1400 typedef ULONG_PTR UI_TYPE; #else typedef ULONG UI_TYPE; #endif typedef HRESULT (STDAPICALLTYPE *pWrapCompressedRTFStream)(LPSTREAM lpCompressedRTFStream, ULONG ulFlags, LPSTREAM FAR * lpUncompressedRTFStream); #define PR_SMTP_ADDRESS (PROP_TAG(PT_STRING8, 0x39fe)) #define PR_SENDER_SMTP_ADDRESS (PROP_TAG(PT_STRING8, 0x0065)) #define PR_SENDER_SMTP_ADDRESS2 (PROP_TAG(PT_STRING8, 0x0c1f)) #ifndef PR_INTERNET_MESSAGE_ID #define PR_INTERNET_MESSAGE_ID 0x1035001E #endif #ifndef PR_BODY_HTML #define PR_BODY_HTML 0x1013001E #endif #ifndef PR_ATTACH_CONTENT_ID #define PR_ATTACH_CONTENT_ID 0x3712001E #endif char *RClientsEmail = "HKEY_LOCAL_MACHINE\\SOFTWARE\\Clients\\Mail"; // Helper classes/functions void RemoveChars(char *Str, int Start, int Len) { memmove(Str, Str + Len, strlen(Str + Len) + 1); } void InsertChars(char *Ins, char *Str, int At) { auto Len = strlen(Ins); memmove(Str + Len, Str, strlen(Str) + 1); memcpy(Str, Ins, Len); } class MapiEntry { public: uchar *Bin; int Len; MapiEntry(SPropValue *v) { Len = 0; Bin = 0; if (v) { Len = v->Value.bin.cb; Bin = new uchar[Len]; if (Bin) { memcpy(Bin, v->Value.bin.lpb, Len); } } } ~MapiEntry() { DeleteArray(Bin); } }; SPropValue *MapiGetField(SRow *Row, int Field) { SPropValue *v = 0; if (Row) { for (unsigned i=0; icValues; i++) { if (PROP_ID(Row->lpProps[i].ulPropTag) == PROP_ID(Field)) { return Row->lpProps + i; } } } return v; } SPropValue *MapiGetProp(IMAPIProp *Props, int Field) { SPropTagArray InTag; InTag.cValues = 1; InTag.aulPropTag[0] = Field; SPropValue *OutTag = 0; ULONG Tags = 0; if (Props) { if (Props->GetProps(&InTag, 0, &Tags, &OutTag) == S_OK) { if (Tags == 1) { return OutTag; } } } return 0; } int MapiCastInt(SPropValue *Val) { if (Val && PROP_TYPE(Val->ulPropTag) == PT_LONG) { return Val->Value.l; } if (Val && PROP_TYPE(Val->ulPropTag) == PT_SHORT) { return Val->Value.i; } return 0; } char *MapiCastString(SPropValue *Val) { if (Val && PROP_TYPE(Val->ulPropTag) == PT_STRING8) { return Val->Value.lpszA; } return 0; } bool MapiCastBinary(SPropValue *Val, void *&Ptr, int &Size) { if (Val && PROP_TYPE(Val->ulPropTag) == PT_BINARY) { Ptr = Val->Value.bin.lpb; Size = Val->Value.bin.cb; return true; } return false; } char *MapiGetPropStr(IMAPIProp *Props, int Field) { SPropValue *Val = MapiGetProp(Props, Field); if (Val) { return MapiCastString(Val); } return 0; } int MapiGetPropInt(IMAPIProp *Props, int Field) { SPropValue *Val = MapiGetProp(Props, Field); if (Val) { return MapiCastInt(Val); } return 0; } bool MapiSetPropStr(IMAPIProp *Props, int Field, const char *Str, bool Unicode = false) { if (Props && Str) { LAssert(PROP_TYPE(Field) == PT_STRING8); SPropValue p; LString n; p.ulPropTag = Field; if (Unicode) { p.Value.lpszA = n = LToNativeCp(Str); } else { p.Value.lpszA = (LPSTR)Str; } bool Status = Props->SetProps(1, &p, 0) == S_OK; return Status; } return false; } bool MapiSetPropLong(IMAPIProp *Props, int Field, ULONG lng) { if (Props) { SPropValue p; p.ulPropTag = Field; p.Value.l = lng; return Props->SetProps(1, &p, 0) == S_OK; } return false; } bool MapiSetPropBool(IMAPIProp *Props, int Field, bool b) { if (Props) { SPropValue p; p.ulPropTag = Field; p.Value.b = b; return Props->SetProps(1, &p, 0) == S_OK; } return false; } bool MapiSetPropDate(IMAPIProp *Props, int Field, const LDateTime &d, bool AdjustTz = true) { if (d.Year()) { // Set sent date SPropValue Prop; Prop.ulPropTag = Field; LDateTime a = d; if (AdjustTz) a.ToUtc(); SYSTEMTIME st; st.wDay = a.Day(); st.wMonth = a.Month(); st.wYear = a.Year(); st.wMinute = a.Minutes(); st.wHour = a.Hours(); st.wSecond = a.Seconds(); st.wMilliseconds = 0; if (SystemTimeToFileTime(&st, &Prop.Value.ft)) { return Props->SetProps(1, &Prop, 0) == S_OK; } } return false; } class LMapiList { LPMAPITABLE List; ULONG Rows; SRowSet *BaseRow; uint32_t i, StartIndex; bool ReleaseList; public: LMapiList(LPMAPITABLE &list, bool release = true) { List = list; list = NULL; Rows = 0; BaseRow = 0; i = 0; StartIndex = 0; ReleaseList = release; if (List) { HRESULT r = List->GetRowCount(0, &Rows); if (Rows && List->SeekRow(BOOKMARK_BEGINNING, 0, NULL) == S_OK) { if (List->QueryRows(Rows, 0, &BaseRow) == S_OK) { // Ok } } } } ~LMapiList() { if (List && ReleaseList) { List->Release(); } } int Index() { return i; } int Length() { return Rows; } bool More() { return (BaseRow && i >= StartIndex && i < BaseRow->cRows + StartIndex); } void Next() { i++; if (BaseRow->cRows < Rows && i-StartIndex >= BaseRow->cRows) { // run into the end of the list section int s = StartIndex + BaseRow->cRows; if (List->QueryRows(Rows-StartIndex, 0, &BaseRow) == S_OK) { StartIndex = s; } } } SRowSet *Current() { if (More()) // in range { return BaseRow + i - StartIndex; } return 0; } SPropValue *GetField(int Field) { if (More()) // in range { return MapiGetField(BaseRow->aRow + i - StartIndex, Field); } return 0; } }; // DEBUG STUFF #ifdef _DEBUG class LRow : public LListItem { SRow *Row; char **Data; int Cols; public: LRow(SRow *row) { Cols = 0; Row = row; if (Row) { Cols = Row->cValues; Data = new char*[Cols]; if (Data) { memset(Data, 0, sizeof(*Data)*Cols); } } } const char *GetText(int i) { SPropValue *Value = (Row)?Row->lpProps+i:0; if (Value) { char Str[256] = ""; DeleteArray(Data[i]); switch (PROP_TYPE(Value->ulPropTag)) { case PT_I2: { sprintf_s(Str, sizeof(Str), "%i", Value->Value.i); break; } case PT_I4: { sprintf_s(Str, sizeof(Str), "%i", Value->Value.l); break; } case PT_R8: { sprintf_s(Str, sizeof(Str), "%f", Value->Value.dbl); break; } case PT_BOOLEAN: { sprintf_s(Str, sizeof(Str), "%s", (Value->Value.b)?"true":"false"); break; } case PT_STRING8: { sprintf_s(Str, sizeof(Str), "%s", Value->Value.lpszA); break; } case PT_ERROR: { sprintf_s(Str, sizeof(Str), "e(%08.8X)", Value->Value.err); break; } case PT_BINARY: { sprintf_s(Str, sizeof(Str), "Bin(%i)", Value->Value.bin.cb); break; } default: { sprintf_s(Str, sizeof(Str), "#TYPE(%04.4X)", PROP_TYPE(Value->ulPropTag)); break; } } if (strlen(Str)>0) { Data[i] = NewStr(Str); } return (Data[i])?Data[i]:""; } return ""; } }; class LShowTable : public LDialog { SRowSet *Table; LList *List; public: LShowTable(LView *parent, SRowSet *table) { SetParent(parent); SetPos(LRect(0, 0, 660, 490)); MoveToCenter(); Name("Show Table"); Table = table; Children.Insert(new LButton(101, 550, 420, 60, 20, "Close")); Children.Insert(List = new LList(100, 10, 10, 600, 400)); if (List && Table) { if (Table->cRows > 0) { SRow *First = Table->aRow; for (unsigned i=0; icValues; i++) { char Str[256]; sprintf_s(Str, sizeof(Str), "%08.8X", First->lpProps[i].ulPropTag); List->AddColumn(Str, 80); } for (unsigned i=0; icRows; i++) { List->Insert(new LRow(Table->aRow+i)); } } } } int OnNotify(LViewI *Ctrl, LNotification n) { if (Ctrl->GetId() == 101) { EndModal(0); } return 0; } }; void ViewTable(LPMAPITABLE Table, LView *Wnd) { LMapiList Lst(Table); if (Lst.Current()) { auto Tbl = new LShowTable(Wnd, Lst.Current()); Tbl->DoModal(NULL); } } #endif // _DEBUG class EntryRef { public: char *DisplayName; int Size; LPENTRYID Entry; EntryRef() { DisplayName = 0; Size = 0; Entry = 0; } bool OpenRoot(LComPtr &Session, UI_TYPE UiHnd, LComPtr &MsgStore, LComPtr &RootFolder) { if (!Session) { LgiTrace("%s:%i - No Session.\n", _FL); return false; } if (Session->OpenMsgStore( UiHnd, Size, // entry bytes Entry, // ptr to entry NULL, // default interface: IMsgStore MAPI_BEST_ACCESS, MsgStore.Set()) != S_OK || !MsgStore) { LgiTrace("%s:%i - OpenMsgStore failed.\n", _FL); return false; } SPropValue *SubTree = MapiGetProp(MsgStore, PR_IPM_SUBTREE_ENTRYID); if (!SubTree) { LgiTrace("%s:%i - Failed to get PR_IPM_SUBTREE_ENTRYID.\n", _FL); return false; } ULONG ObjType; if (MsgStore->OpenEntry(SubTree->Value.bin.cb, (LPENTRYID)SubTree->Value.bin.lpb, NULL, MAPI_MODIFY, &ObjType, (IUnknown**) RootFolder.Set()) != S_OK || !RootFolder) { LgiTrace("%s:%i - OpenEntry failed.\n", _FL); MsgStore.Release(); return false; } return true; } }; class MessageStores : public List { LPMAPITABLE MsgStores; public: bool Status; MessageStores(LPMAPISESSION Session) { Status = false; MsgStores = 0; if (Session->GetMsgStoresTable(0, &MsgStores) == S_OK && MsgStores) { for (LMapiList Lst(MsgStores, false); Lst.More(); Lst.Next()) { SPropValue *DisplayName = Lst.GetField(PR_DISPLAY_NAME); SPropValue *Entry = Lst.GetField(PR_ENTRYID); if (DisplayName && Entry) { LAutoPtr Ref(new EntryRef); if (Ref) { Ref->DisplayName = MapiCastString(DisplayName); void *p = 0; int s = 0; if (Ref->DisplayName && MapiCastBinary(Entry, p, s)) { Ref->Size = s; Ref->Entry = (ENTRYID*) p; Insert(Ref.Release()); } } } } Status = true; } } ~MessageStores() { if (MsgStores) MsgStores->Release(); DeleteObjects(); } }; class ChooseMessageStoreDlg : public LDialog { ScribeWnd *App; LCombo *CStore; LEdit *Folder; bool InitOk; bool Directory; MessageStores *Stores; public: int Size; LPENTRYID Entry; EntryRef *Ref; LString FolderName; ChooseMessageStoreDlg(ScribeWnd *Wnd, LPMAPISESSION Session, bool Dir = true) { App = Wnd; Size = 0; Entry = 0; InitOk = false; Directory = Dir; Ref = 0; Stores = 0; CStore = 0; Folder = 0; if (LoadFromResource(IDD_OUTLOOK_IMPORT)) { Stores = new MessageStores(Session); if (Stores && Stores->Status) { if (GetViewById(IDC_MSG_STORE, CStore)) { InitOk = true; for (auto e: *Stores) CStore->Insert(e->DisplayName); } } if (GetViewById(IDC_FOLDER, Folder)) { Folder->Name("/"); Folder->Enabled(false); } } MoveToCenter(); } ~ChooseMessageStoreDlg() { DeleteObj(Stores); } bool InitCheck() { return InitOk; } void OnCreate() { LViewI *v = FindControl(IDC_TABLE); if (v) { LRect c = GetClient(); c.Inset(10, 10); v->SetPos(c); } } int OnNotify(LViewI *Ctrl, LNotification n) { switch (Ctrl->GetId()) { case IDC_SET_FOLDER: { if (!Folder) break; auto Dlg = new FolderDlg(this, App); Dlg->DoModal([this, Dlg](auto dlg, auto ctrlId) { if (ctrlId) this->Folder->LView::Name(Dlg->Get()); delete dlg; }); break; } case IDOK: { if (CStore) { Ref = Stores->ItemAt((int)CStore->Value()); if (Ref) { Size = Ref->Size; Entry = Ref->Entry; } } if (Directory && Folder) FolderName = Folder->Name(); EndModal(1); break; } case IDCANCEL: { EndModal(0); break; } } return 0; } }; // Import / Export parameters class ImportParams { public: ScribeWnd *App; Progress *Prog; Progress *ItemProg; bool AllFolders; LString::Array Import; ImportParams() { AllFolders = false; App = NULL; Prog = NULL; ItemProg = NULL; } ~ImportParams() { } }; class ExportParams { public: ScribeWnd *App; Progress *Prog; Progress *ItemProg; bool AllFolders; List Include; List Export; List Exclude; ExportParams() { AllFolders = false; App = 0; Prog = 0; ItemProg = 0; } }; int StrCmp(char *a, char *b, NativeInt d) { return _stricmp(a, b); } // Importer class class OutlookIO { friend class MailMapiSource; friend class ImportDlg; friend class ExportDlg; ScribeWnd *App; ScribeAccount *Account; LAutoString DefClient; HINSTANCE hMapi; LComPtr Session; LComPtr MsgStore; UI_TYPE Ui; LComPtr AddrBook; MAPIINITIALIZE *MAPIInitialize; MAPILOGONEX *MAPILogonEx; MAPIUNINITIALIZE *MAPIUninitialize; MAPIALLOCATEBUFFER *MAPIAllocateBuffer; MAPIFREEBUFFER *MAPIFreeBuffer; pWrapCompressedRTFStream WrapCompressedRTFStream; bool ImportEmail(ScribeFolder *Folder, Mail *&To, IMessage *From, bool Post); public: OutlookIO(ScribeWnd *Wnd, int Flags, ScribeAccount *Account = 0); ~OutlookIO(); bool LoadSession(LViewI *Parent, char *ResetClient); LComPtr &GetSession() { return Session; } UI_TYPE GetUiHnd() { return Ui; } // Import functions bool Import( ImportParams *P, IMAPIFolder *In, ScribeFolder *Out, LString::Array &Path); void ImportPersonalAddressBook(std::function callback); // Export functions bool Export( ExportParams *P, ScribeFolder *In, IMAPIFolder *Out, int Depth = 0); bool ExportCalendar(Calendar *c, IMessage *m); int CountFolders(ScribeFolder *f); // Workers bool ImportItem(ScribeFolder *Out, IMAPIFolder *In, SPropValue *EntryId); int FolderType(IMAPIFolder *In); }; // UI classes class AddFolderDlg : public LDialog { OutlookIO *Io; LTree *Tree; void Load(IMAPIFolder *f, LTreeItem *i) { if (f && i) { LPMAPITABLE Folders = 0; if (f->GetHierarchyTable(0, &Folders) == S_OK) { for (LMapiList Lst(Folders); Lst.More(); Lst.Next()) { SPropValue *v = Lst.GetField(PR_ENTRYID); LString Name = LFromNativeCp(MapiCastString(Lst.GetField(PR_DISPLAY_NAME))); if (v && Name) { LTreeItem *ChildItem = new LTreeItem; if (ChildItem) { ChildItem->SetText(Name); i->Insert(ChildItem); i->Expanded(true); IMAPIFolder *ChildFolder = 0; ULONG Type; if (f->OpenEntry(v->Value.bin.cb, (LPENTRYID)v->Value.bin.lpb, NULL, MAPI_BEST_ACCESS, &Type, (IUnknown**)&ChildFolder) == S_OK && ChildFolder) { Load(ChildFolder, ChildItem); ChildFolder->Release(); } } } } } } } public: LAutoString Path; AddFolderDlg(LView *parent, OutlookIO *io, EntryRef *Store) { Io = io; SetParent(parent); if (LoadFromResource(IDD_OUTLOOK_ADD_FLD)) { MoveToCenter(); GetViewById(IDC_FOLDERS, Tree); auto Session = Io->GetSession(); if (Tree && Session && Store) { LTreeItem *RootItem = new LTreeItem; if (RootItem) { RootItem->SetText(Store->DisplayName); Tree->Insert(RootItem); RootItem->Expanded(true); LComPtr MsgStore; LComPtr RootFolder; if (Store->OpenRoot(Session, Io->GetUiHnd(), MsgStore, RootFolder)) Load(RootFolder, RootItem); } } } } int OnNotify(LViewI *c, LNotification n) { switch (c->GetId()) { case IDOK: { LArray p; for (LTreeItem *s = Tree ? Tree->Selection() : 0; s; s = s->GetParent()) { p.AddAt(0, s); } LStringPipe n; for (unsigned i=1; iGetText()); } Path.Reset(n.NewStr()); LAssert(Path != NULL); // Fall thru } case IDCANCEL: { EndModal(c->GetId() == IDOK); break; } } return 0; } }; class IoDlg : public LDialog { protected: ScribeWnd *App; List Clients; OutlookIO *Io; char *DefClient; LAutoPtr Stores; LList *SrcFolders; LComPtr *MapiFolder; ScribeFolder **OurFolder; public: IoDlg( ScribeWnd *app, OutlookIO *io, LComPtr *mapifolder, ScribeFolder *&scribefolder) { App = app; Io = io; DefClient = 0; SrcFolders = 0; MapiFolder = mapifolder; OurFolder = &scribefolder; } ~IoDlg() { Clients.DeleteArrays(); DeleteArray(DefClient); } virtual void AddFolder() {} virtual void OnLogin(bool e) {} void InitSrcClients() { LRegKey Email(false, RClientsEmail); LCombo *SrcClient; if (Email.GetKeyNames(Clients) && GetViewById(IDC_CLIENT, SrcClient)) { Clients.Sort(StrCmp); DefClient = NewStr(Email.GetStr()); int i = 0; for (auto c: Clients) { SrcClient->Insert(c); if (DefClient && _stricmp(DefClient, c) == 0) { SrcClient->Value(i); } i++; } } } void AddPath(char *p) { if (SrcFolders && p) { bool Has = false; for (auto i : *SrcFolders) { const char *s = i->GetText(0); if (s && _stricmp(s, p) == 0) { Has = true; break; } } if (!Has) { LListItem *i = new LListItem; if (i) { i->SetText(p); SrcFolders->Insert(i); SrcFolders->ResizeColumnsToContent(); } } } } int OnNotify(LViewI *v, LNotification n) { bool Ok = false; switch (v->GetId()) { case IDC_LOGIN: { const char *NewClient = GetCtrlName(IDC_CLIENT); if (!NewClient) { LgiMsg(this, "Unknown Client Name.", AppName); break; } if (!Io->LoadSession(this, DefClient)) { LgiMsg(this, "Outlook LoadSession failed.\n", AppName); break; } OnLogin(true); Stores.Reset(new MessageStores(Io->GetSession())); LCombo *StoreCbo; if (!Stores || !GetViewById(IDC_MSG_STORE, StoreCbo)) { LgiMsg(this, "No store combo?\n", AppName); break; } for (auto e: *Stores) StoreCbo->Insert(e->DisplayName); StoreCbo->Value(0); break; } case IDC_ADD_SRC_FOLDER: { AddFolder(); break; } case IDC_DEL_SRC_FOLDER: { if (!SrcFolders) { LgiTrace("%s:%i - SrcFolders is NULL.\n", _FL); break; } List s; if (!SrcFolders->GetSelection(s)) { LgiTrace("%s:%i - GetSelection failed.\n", _FL); break; } s.DeleteObjects(); break; } } return 0; } }; class ImportDlg : public IoDlg { ImportParams *Params; public: ImportDlg( ScribeWnd *app, ImportParams *params, OutlookIO *io, LComPtr *mapifolder, ScribeFolder *&scribefolder) : IoDlg(app, io, mapifolder, scribefolder) { SetParent(App = app); Params = params; if (LoadFromResource(IDD_OUTLOOK_IMPORT)) { MoveToCenter(); OnLogin(false); GetViewById(IDC_SRC_FOLDERS, SrcFolders); LVariant s; if (App->GetOptions()->GetValue(OPT_OutlookImportSrc, s) && s.Str()) { - LToken t(s.Str(), ","); + auto t = s.LStr().SplitDelimit(","); for (unsigned i=0; iGetOptions()->GetValue(OPT_OutlookImportDst, s) && s.Str()) SetCtrlName(IDC_FOLDER, s.Str()); else { auto Cur = App->GetCurrentFolder(); if (Cur) { auto Path = Cur->GetPath(); if (Path) SetCtrlName(IDC_FOLDER, Path); } } if (App->GetOptions()->GetValue(OPT_OutlookImportAll, s)) { SetCtrlValue(IDC_ALL, s.CastInt32()); } InitSrcClients(); } } void OnLogin(bool e) { SetCtrlEnabled(IDC_CLIENT, !e); SetCtrlEnabled(IDC_LOGIN, !e); SetCtrlEnabled(IDC_MSG_STORE, e); SetCtrlEnabled(IDC_ALL, e); SetCtrlEnabled(IDC_SRC_FOLDERS, e); SetCtrlEnabled(IDC_ADD_SRC_FOLDER, e); SetCtrlEnabled(IDC_DEL_SRC_FOLDER, e); SetCtrlEnabled(IDC_SET_FOLDER, e); SetCtrlEnabled(IDOK, e); SetCtrlEnabled(IDC_FOLDER, false); } void AddFolder() { if (Stores) { EntryRef *e = (*Stores)[(int)GetCtrlValue(IDC_MSG_STORE)]; if (!e) { auto Dlg = new AddFolderDlg(this, Io, e); Dlg->DoModal([this, Dlg](auto dlg, auto ctrlId) { if (ctrlId) AddPath(Dlg->Path); delete dlg; }); } } } int OnNotify(LViewI *v, LNotification n) { bool Ok = false; switch (v->GetId()) { case IDC_SET_FOLDER: { auto Dlg = new FolderDlg(this, App); Dlg->DoModal([this, Dlg](auto dlg, auto ctrlId) { if (ctrlId) SetCtrlName(IDC_FOLDER, Dlg->Get()); delete dlg; }); break; } case IDOK: { Ok = true; // fall thru } case IDCANCEL: { LVariant k; const char *s; if (SrcFolders) { Params->Import.Empty(); Params->Import.SetFixedLength(false); for (auto i : *SrcFolders) Params->Import.New() = i->GetText(0); Params->Import.SetFixedLength(true); auto s = LString(",").Join(Params->Import); if (s) App->GetOptions()->SetValue(OPT_OutlookImportSrc, k = s); else App->GetOptions()->DeleteValue(OPT_OutlookImportSrc); } s = GetCtrlName(IDC_FOLDER); if (s) { App->GetOptions()->SetValue(OPT_OutlookImportDst, k = s); if (Ok) *OurFolder = App->GetFolder(s); } else { App->GetOptions()->DeleteValue(OPT_OutlookImportDst); } Params->AllFolders = GetCtrlValue(IDC_ALL) != 0; App->GetOptions()->SetValue(OPT_OutlookImportAll, k = (int)Params->AllFolders); if (Ok && Io->GetSession() && Stores) { EntryRef *e = (*Stores)[(int)GetCtrlValue(IDC_MSG_STORE)]; if (e) { e->OpenRoot(Io->GetSession(), Io->GetUiHnd(), Io->MsgStore, *MapiFolder); } } EndModal(v->GetId()); break; } } return IoDlg::OnNotify(v, n); } }; class ExportDlg : public IoDlg { ExportParams *Params; public: ExportDlg( ScribeWnd *app, ExportParams *params, OutlookIO *io, LComPtr *MapiFolder, ScribeFolder *&ScribeFolder) : IoDlg(app, io, MapiFolder, ScribeFolder) { SetParent(app); Params = params; if (LoadFromResource(IDD_OUTLOOK_EXPORT)) { MoveToCenter(); OnLogin(false); GetViewById(IDC_SRC_FOLDERS, SrcFolders); SetCtrlValue(IDC_NO_SPAM_TRASH, true); LVariant v; if (App->GetOptions()->GetValue(OPT_OutlookExportSrc, v) && v.Str()) { - LToken t(v.Str(), ","); + auto t = v.LStr().SplitDelimit(","); for (unsigned i=0; iGetOptions()->GetValue(OPT_OutlookExportDst, v)) { SetCtrlName(IDC_FOLDER, v.Str()); } if (App->GetOptions()->GetValue(OPT_OutlookExportAll, v)) { SetCtrlValue(IDC_ALL, v.CastInt32()); } if (App->GetOptions()->GetValue(OPT_OutlookExportExclude, v)) { SetCtrlValue(IDC_NO_SPAM_TRASH, v.CastInt32()); } InitSrcClients(); } } void OnLogin(bool e) { SetCtrlEnabled(IDC_CLIENT, !e); SetCtrlEnabled(IDC_LOGIN, !e); SetCtrlEnabled(IDC_MSG_STORE, e); SetCtrlEnabled(IDC_ALL, e); SetCtrlEnabled(IDC_NO_SPAM_TRASH, e); SetCtrlEnabled(IDC_SRC_FOLDERS, e); SetCtrlEnabled(IDC_ADD_SRC_FOLDER, e); SetCtrlEnabled(IDC_DEL_SRC_FOLDER, e); SetCtrlEnabled(IDC_SET_FOLDER, e); SetCtrlEnabled(IDOK, e); SetCtrlEnabled(IDC_FOLDER, false); } void AddFolder() { auto Fs = new FolderDlg(this, App); Fs->DoModal([this, Fs](auto dlg, auto ctrlId) { if (ctrlId && Fs->Get()) AddPath(Fs->Get()); delete dlg; }); } LComPtr GetSubFolderByName(IMAPIFolder *f, char *name) { LComPtr ChildFolder; if (f && name) { LPMAPITABLE Folders = 0; if (f->GetHierarchyTable(0, &Folders) == S_OK) { for (LMapiList Lst(Folders); Lst.More(); Lst.Next()) { SPropValue *v = Lst.GetField(PR_ENTRYID); char *Name = MapiCastString(Lst.GetField(PR_DISPLAY_NAME)); if (v && Name) { if (_stricmp(Name, name) == 0) { ULONG Type; f->OpenEntry(v->Value.bin.cb, (LPENTRYID)v->Value.bin.lpb, NULL, MAPI_BEST_ACCESS, &Type, (IUnknown**)ChildFolder.Set()); } } } } } return ChildFolder; } int OnNotify(LViewI *c, LNotification n) { switch (c->GetId()) { case IDC_SET_FOLDER: { if (Stores) { EntryRef *e = (*Stores)[(int)GetCtrlValue(IDC_MSG_STORE)]; if (e) { auto Dlg = new AddFolderDlg(this, Io, e); Dlg->DoModal([this, Dlg](auto dlg, auto id) { if (id) SetCtrlName(IDC_FOLDER, Dlg->Path); delete dlg; }); } } break; } case IDOK: { Params->Include.Empty(); Params->AllFolders = GetCtrlValue(IDC_ALL) != 0; for (auto i : *SrcFolders) { ScribeFolder *f = App->GetFolder(i->GetText(0)); if (f) { if (!Params->Export.HasItem(f)) { Params->Export.Insert(f); } if (!Params->Include.HasItem(f)) { Params->Include.Insert(f); ScribeFolder *p = f; while (p = p->GetFolder()) { if (!Params->Include.HasItem(p)) { Params->Include.Insert(p); } } } } } if (GetCtrlValue(IDC_NO_SPAM_TRASH)) { Params->Exclude.Insert(App->GetFolder("/Spam")); Params->Exclude.Insert(App->GetFolder(FOLDER_TRASH)); } // Get the scribe folder to start at... *OurFolder = App->GetFolder("/"); // Get the mapi folder to start at.. if (Io->GetSession() && Stores) { EntryRef *e = (*Stores)[(int)GetCtrlValue(IDC_MSG_STORE)]; if (e) { // Open the store... e->OpenRoot(Io->GetSession(), Io->GetUiHnd(), Io->MsgStore, *MapiFolder); // Drill down to the select sub-folder... - const char *DstFolder = GetCtrlName(IDC_FOLDER); - if (DstFolder) + auto t = LString(GetCtrlName(IDC_FOLDER)).SplitDelimit("/"); + if (t.Length() > 0) { - LToken t(DstFolder, "/"); - if (t.Length() > 0) + LComPtr f = *MapiFolder; + for (unsigned i=0; i f = *MapiFolder; - for (unsigned i=0; iGetText(0)); } char *Fld = p.NewStr(); if (Fld) { App->GetOptions()->SetValue(OPT_OutlookExportSrc, v = Fld); DeleteArray(Fld); } else { App->GetOptions()->DeleteValue(OPT_OutlookExportSrc); } App->GetOptions()->SetValue(OPT_OutlookExportDst, v = GetCtrlName(IDC_FOLDER)); App->GetOptions()->SetValue(OPT_OutlookExportAll, v = GetCtrlValue(IDC_ALL)); App->GetOptions()->SetValue(OPT_OutlookExportExclude, v = GetCtrlValue(IDC_NO_SPAM_TRASH)); EndModal(c->GetId() == IDOK); break; } } return IoDlg::OnNotify(c, n); } }; void Import_Outlook(ScribeWnd *Wnd, int Flags) { OutlookIO Imp(Wnd, Flags); } void Export_Outlook(ScribeWnd *Wnd) { OutlookIO Exp(Wnd, EXP_OUTLOOK_EMAIL); } OutlookIO::OutlookIO(ScribeWnd *Wnd, int Flags, ScribeAccount *account) { App = Wnd; hMapi = NULL; Ui = (UI_TYPE) ((Wnd) ? Wnd->Handle() : 0); MAPIInitialize = NULL; MAPILogonEx = NULL; MAPIUninitialize = NULL; MAPIAllocateBuffer = NULL; MAPIFreeBuffer = NULL; WrapCompressedRTFStream = NULL; Account = account; ImportParams IParams; IParams.App = App; ExportParams EParams; EParams.App = App; LComPtr MapiFolder; ScribeFolder *OurFolder = NULL; #if 0 // FIXME if (Flags == IMP_OUTLOOK) { ImportDlg Dlg(Wnd, &IParams, this, &MapiFolder, OurFolder); if (Dlg.DoModal() == IDOK) { } else return; } else if (Flags == EXP_OUTLOOK_EMAIL) { ExportDlg Dlg(Wnd, &EParams, this, &MapiFolder, OurFolder); if (Dlg.DoModal() == IDOK) { } else return; } if (Session && MapiFolder && OurFolder) { if (Flags == EXP_OUTLOOK_EMAIL) { LProgressDlg *Prog = new LProgressDlg(Wnd); if (Prog) { Prog->SetDescription("Initializing..."); if (EParams.Export.Length()) { Prog->SetRange(EParams.Export.Length()); } else { LMailStore *Ms = App->GetDefaultMailStore(); Prog->SetRange(Ms ? CountFolders(Ms->Root) : 0); } EParams.Prog = Prog->ItemAt(0); EParams.ItemProg = Prog->Push(); if (EParams.ItemProg) { EParams.ItemProg->SetDescription("Items..."); } } for (auto *f = OurFolder->GetChildFolder(); f; f = f->GetNextFolder()) { Export(&EParams, f, MapiFolder); } DeleteObj(Prog); } else { if (Flags == IMP_OUTLOOK_PAB) { ImportPersonalAddressBook(); } else { LProgressDlg *Prog = new LProgressDlg(Wnd); if (Prog) { Prog->SetDescription("Initializing..."); if (IParams.Import.Length()) { Prog->SetRange(IParams.Import.Length()); } else { LMailStore *Ms = App->GetDefaultMailStore(); Prog->SetRange(Ms ? CountFolders(Ms->Root) : 0); } IParams.Prog = Prog->ItemAt(0); IParams.ItemProg = Prog->Push(); if (IParams.ItemProg) { IParams.ItemProg->SetDescription("Items..."); } } LString::Array Path; Import(&IParams, MapiFolder, OurFolder, Path); DeleteObj(Prog); } } } #endif if (MsgStore) MsgStore.Release(); } OutlookIO::~OutlookIO() { if (AddrBook) AddrBook.Release(); if (Session) { Session->Logoff(0, 0, 0); Session.Release(); } if (MsgStore) MsgStore.Release(); if (MAPIUninitialize) { MAPIUninitialize(); } if (hMapi) { FreeLibrary(hMapi); hMapi = NULL; } } bool OutlookIO::LoadSession(LViewI *Parent, char *ResetClient) { bool Status = false; char Cur[MAX_PATH_LEN]; GetCurrentDirectory(sizeof(Cur), Cur); DefClient.Reset(NewStr(ResetClient)); HRESULT Error = S_OK; hMapi = LoadLibraryA("mapi32.dll"); if (hMapi) { MAPIInitialize = (MAPIINITIALIZE*) GetProcAddress(hMapi, "MAPIInitialize"); MAPILogonEx = (MAPILOGONEX*) GetProcAddress(hMapi, "MAPILogonEx"); MAPIAllocateBuffer = (MAPIALLOCATEBUFFER*) GetProcAddress(hMapi, "MAPIAllocateBuffer"); MAPIFreeBuffer = (MAPIFREEBUFFER*) GetProcAddress(hMapi, "MAPIFreeBuffer"); WrapCompressedRTFStream = (pWrapCompressedRTFStream) GetProcAddress(hMapi, "WrapCompressedRTFStream"); if (MAPIInitialize && MAPILogonEx && MAPIAllocateBuffer && MAPIFreeBuffer && MAPIInitialize(NULL) == S_OK) { MAPIUninitialize = (MAPIUNINITIALIZE*) GetProcAddress(hMapi, "MAPIUninitialize"); if (!Account) { if (MAPILogonEx(Parent ? (UI_TYPE)Parent->Handle() : Ui, NULL, NULL, MAPI_LOGON_UI | MAPI_EXTENDED, Session.Set()) == S_OK && Session) { SetCurrentDirectory(Cur); Status = true; } else { LgiMsg(App, LLoadString(IDS_MAPI_LOGIN_FAILED), AppName); } } } else { LgiMsg(App, LLoadString(IDS_ERROR_MAPI_INIT_FAILED), AppName); } } else { LgiMsg(App, LLoadString(IDS_ERROR_MAPI_NOT_INSTALLED), AppName); } if (!Status && hMapi) { MAPIUninitialize(); MAPIUninitialize = NULL; MAPIInitialize = NULL; MAPILogonEx = NULL; MAPIAllocateBuffer = NULL; MAPIFreeBuffer = NULL; WrapCompressedRTFStream = NULL; FreeLibrary(hMapi); hMapi = NULL; } return Status; } Mail *MatchEmail(ScribeFolder *f, Mail *m1) { f->LoadThings(); for (auto t: f->Items) { Mail *m2 = t->IsMail(); if (m2) { const char *id1, *id2; if ((id1 = m1->GetMessageId()) && (id2 = m2->GetMessageId()) && strcmp(id1, id2) == 0) { return m2; } } } for (auto t: f->Items) { Mail *m2 = t->IsMail(); if (m2) { if (m1->GetSubject() && m2->GetSubject() && strcmp(m1->GetSubject(), m2->GetSubject()) == 0 && *m1->GetDateSent() == *m2->GetDateSent()) { return m2; } } } return 0; } int MapNetCpToWin(int Cp) { switch (Cp) { case 708: return 1256; case 720: return 1256; case 28596: return 1256; case 10004: return 1256; case 1256: return 1256; case 775: return 1257; case 28594: return 1257; case 1257: return 1257; case 852: return 1250; case 28592: return 1250; case 10029: return 1250; case 1250: return 1250; case 51936: return 936; case 936: return 936; case 52936: return 936; case 10008: return 936; case 950: return 950; case 20000: return 950; case 20002: return 950; case 10002: return 950; case 866: return 1251; case 28595: return 1251; case 20866: return 1251; case 21866: return 1251; case 10007: return 1251; case 1251: return 1251; case 29001: return 1252; case 20106: return 1252; case 737: return 1253; case 28597: return 1253; case 10006: return 1253; case 1253: return 1253; case 869: return 1253; case 862: return 1255; case 38598: return 1255; case 28598: return 1255; case 10005: return 1255; case 1255: return 1255; case 20420: return 1256; case 20880: return 1251; case 21025: return 1251; case 20277: return 1252; case 1142: return 1252; case 20278: return 1252; case 1143: return 1252; case 1147: return 1252; case 20273: return 1252; case 1141: return 1252; case 875: return 1253; case 20423: return 1253; case 20424: return 1255; case 20871: return 1252; case 1149: return 1252; case 1148: return 1252; case 20280: return 1252; case 1144: return 1252; case 50930: return 932; case 50939: return 932; case 50931: return 932; case 20290: return 932; case 50933: return 949; case 20833: return 949; case 870: return 1250; case 50935: return 936; case 20284: return 1252; case 1145: return 1252; case 20838: return 874; case 50937: return 950; case 1026: return 1254; case 20905: return 1254; case 20285: return 1252; case 1146: return 1252; case 37: return 1252; case 1140: return 1252; case 861: return 1252; case 10079: return 1252; case 57006: return 57006; case 57003: return 57003; case 57002: return 57002; case 57010: return 57010; case 57008: return 57008; case 57009: return 57009; case 57007: return 57007; case 57011: return 57011; case 57004: return 57004; case 57005: return 57005; case 51932: return 932; case 50220: return 932; case 50222: return 932; case 50221: return 932; case 10001: return 932; case 932: return 932; case 949: return 949; case 51949: return 949; case 50225: return 949; case 1361: return 1361; case 10003: return 949; case 28593: return 1254; case 28605: return 1252; case 20108: return 1252; case 437: return 1252; case 20107: return 1252; case 874: return 874; case 857: return 1254; case 28599: return 1254; case 10081: return 1254; case 1254: return 1254; case 1200: return 1200; case 1201: return 1200; case 65000: return 1200; case 65001: return 1200; case 20127: return 1252; case 1258: return 1258; case 850: return 1252; case 20105: return 1252; case 28591: return 1252; case 10000: return 1252; case 1252: return 1252; } return 0; } LAutoString MapiToUtf(const char *Charset, const char *Text) { LAutoString s; char16 *w = (char16*) LNewConvertCp(LGI_WideCharset, Text, Charset ? Charset : LAnsiToLgiCp()); if (w) { s.Reset(WideToUtf8(w)); DeleteArray(w); } return s; } bool OutlookIO::ImportEmail(ScribeFolder *Folder, Mail *&To, IMessage *From, bool Post) { if (!To) return false; if (!From) { To->DecRef(); return false; } char *MsgId = MapiGetPropStr(From, PR_INTERNET_MESSAGE_ID); if (MsgId) { To->SetMessageId(MsgId); } // Get the codepage... int CodePage = MapNetCpToWin(MapiGetPropInt(From, 0x3FDE0003)); const char *Charset = LAnsiToLgiCp(CodePage); // Get Internet Header fairly early in the peice to allow us to // extract the codepage for the body of the message. char *Header = MapiGetPropStr(From, PR_TRANSPORT_MESSAGE_HEADERS); if (Header) { To->SetInternetHeader(Header); } // Get subject char *Subject = MapiGetPropStr(From, PR_SUBJECT); if (Subject) { LAutoString a = MapiToUtf(Charset, Subject); To->SetSubject(a); } // Get sent date SPropValue *Sent = MapiGetProp(From, PR_CLIENT_SUBMIT_TIME); if (Sent) { SYSTEMTIME st; FileTimeToSystemTime(&Sent->Value.ft, &st); LDateTime dt; dt.Day(st.wDay); dt.Month(st.wMonth); dt.Year(st.wYear); dt.Minutes(st.wMinute); dt.Hours(st.wHour); dt.Seconds(st.wSecond); To->SetDateSent(&dt); } // Do replication side of things... Mail *Match = MatchEmail(Folder, To); if (Match) { To->DecRef(); return true; } // Get from char *SenderEmail = MapiGetPropStr(From, PR_SENDER_EMAIL_ADDRESS); char *SenderName = MapiGetPropStr(From, PR_SENT_REPRESENTING_NAME); if (!SenderName) SenderName = MapiGetPropStr(From, PR_SENDER_NAME); To->GetFrom()->SetStr(FIELD_NAME, SenderName); if (SenderEmail && strchr(SenderEmail, '@')) { To->GetFrom()->SetStr(FIELD_EMAIL, SenderEmail); } else { // Ok we have to look up their email address ourselves. SPropValue *EmailEntryId = MapiGetProp(From, PR_SENDER_ENTRYID); if (EmailEntryId) { if (!AddrBook) Session->OpenAddressBook(Ui, NULL, 0, AddrBook.Set()); if (AddrBook) { ULONG Type = 0; IMailUser *User = 0; if (AddrBook->OpenEntry(EmailEntryId->Value.bin.cb, (LPENTRYID) EmailEntryId->Value.bin.lpb, 0, MAPI_BEST_ACCESS, &Type, (IUnknown**)&User) == S_OK && User) { char *E1 = MapiGetPropStr(User, PR_EMAIL_ADDRESS); char *E2 = MapiGetPropStr(User, PR_SMTP_ADDRESS); if (E1 && strchr(E1, '@')) { To->GetFrom()->SetStr(FIELD_EMAIL, E1); } else if (E2 && strchr(E2, '@')) { To->GetFrom()->SetStr(FIELD_EMAIL, E2); } } } } } // for all recipients LPMAPITABLE Recipients = 0; if (From->GetRecipientTable(0, &Recipients) == S_OK && Recipients) { for (LMapiList Lst(Recipients); Lst.More(); Lst.Next()) { SPropValue *Name = Lst.GetField(PR_DISPLAY_NAME); SPropValue *Type = Lst.GetField(PR_RECIPIENT_TYPE); SPropValue *Email1 = Lst.GetField(PR_EMAIL_ADDRESS); SPropValue *Email2 = Lst.GetField(PR_SMTP_ADDRESS); if (Name || Email1 || Email2) { LDataPropI *r = To->GetTo()->Create(To->GetObject()->GetStore()); if (r) { if (Name) { r->SetStr(FIELD_NAME, MapiCastString(Name)); } char *E1 = (Email1) ? MapiCastString(Email1) : 0; char *E2 = (Email2) ? MapiCastString(Email2) : 0; if (E1 && strchr(E1, '@')) { r->SetStr(FIELD_EMAIL, E1); } else if (E2 && strchr(E2, '@')) { r->SetStr(FIELD_EMAIL, E2); } if (Type) { LONG Flags = Type->Value.ul; if (Flags & MAPI_TO) { r->SetInt(FIELD_CC, 0); } else if (Flags & MAPI_CC) { r->SetInt(FIELD_CC, 1); } else if (Flags & MAPI_BCC) { r->SetInt(FIELD_CC, 2); } } To->GetTo()->Insert(r); } } } } // Get Body/Text char *Body = MapiGetPropStr(From, PR_BODY); if (Body) { LAutoString a = MapiToUtf(Charset, Body); To->SetBody(a); To->SetBodyCharset("utf-8"); } char *Html = MapiGetPropStr(From, PR_BODY_HTML); if (Html) { To->SetHtml(Html); } else { // Rtf/HTML IStream *Comp = 0; if (WrapCompressedRTFStream && From->OpenProperty( PR_RTF_COMPRESSED, &IID_IStream, 0, 0, (IUnknown**) &Comp) == S_OK) { IStream *Uncomp = 0; if (WrapCompressedRTFStream(Comp, 0, &Uncomp) == S_OK) { LStringPipe p; char Buf[1024]; ULONG r; while (Uncomp->Read(Buf, sizeof(Buf), &r) == S_OK) { if (r) { p.Push(Buf, r); } else break; } char *Rtf = p.NewStr(); if (Rtf) { { int i=0; char p[256]; for (; true; i++) { #ifdef WINDOWS sprintf_s(p, sizeof(p), "c:\\temp\\rtf-%i.txt", i); #else sprintf_s(p, sizeof(p), "/tmp/rtf-%i.txt", i); #endif if (!LFileExists(p)) break; } LFile f; if (f.Open(p, O_WRITE)) { f.Write(Rtf, strlen(Rtf)); } } LAutoString HtmlStr(MsRtfToHtml(Rtf)); To->SetHtml(HtmlStr); DeleteArray(Rtf); } Uncomp->Release(); } Comp->Release(); } } // Get Attachments LPMAPITABLE Attachments = 0; if (From->GetAttachmentTable(0, &Attachments) == S_OK && Attachments) { for (LMapiList Lst(Attachments); Lst.More(); Lst.Next()) { SPropValue *Num = Lst.GetField(PR_ATTACH_NUM); if (!Num) continue; IAttach *Attach = 0; if (From->OpenAttach(Num->Value.ul, NULL, MAPI_BEST_ACCESS, &Attach) != S_OK || !Attach) continue; SPropValue *Method = MapiGetProp(Attach, PR_ATTACH_METHOD); if (Method) { char *AttachName = MapiGetPropStr(Attach, PR_ATTACH_LONG_FILENAME); char *ContentId = MapiGetPropStr(Attach, PR_ATTACH_CONTENT_ID); switch (Method->Value.ul) { case ATTACH_BY_VALUE: { Attachment *File = 0; IStream *Stream = 0; if (Attach->OpenProperty(PR_ATTACH_DATA_BIN, &IID_IStream, 0, 0, (IUnknown**)&Stream) == S_OK && Stream) { STATSTG Stat; if (Stream->Stat(&Stat, STATFLAG_DEFAULT) == S_OK) { int Size = Stat.cbSize.LowPart; char *Data = new char[Size]; if (Data) { int Block = 128 << 10; char *p = Data; for (int i=0; iRead(p, (ULONG)Len, NULL) != S_OK) { break; } p += Len; } if (p == Data + Size) { File = new Attachment(Folder->App); if (File) { File->Set(Data, Size); } } DeleteArray(Data); } } Stream->Release(); } if (File) { char *MimeType = MapiGetPropStr(Attach, PR_ATTACH_MIME_TAG); File->SetMimeType(MimeType ? MimeType : "application/octet-stream"); if (AttachName) { File->SetName(AttachName); } else { char Str[256]; sprintf_s(Str, sizeof(Str), "file%i", Num->Value.ul); File->SetName(Str); } if (ContentId) { File->SetContentId(ContentId); } To->AttachFile(File); // remove attachment from memory if (File->GetObject()) { /* FIXME File->Store->Object = 0; File->Store = 0; DeleteObj(File); */ } } break; } case ATTACH_BY_REFERENCE: case ATTACH_BY_REF_RESOLVE: case ATTACH_BY_REF_ONLY: { char *FileName = MapiGetPropStr(Attach, PR_ATTACH_LONG_PATHNAME); if (FileName) { To->AttachFile(App, FileName); } break; } case ATTACH_EMBEDDED_MSG: { break; } } } Attach->Release(); } } // Set Sent/Received/Read/Unread Flags SPropValue *Flags = MapiGetProp(From, PR_MESSAGE_FLAGS); if (Flags) { ulong f = 0; if (Flags->Value.ul & (MSGFLAG_SUBMIT | MSGFLAG_UNSENT) && !Post) { f |= MAIL_CREATED; } else { f |= MAIL_RECEIVED; } if (Flags->Value.ul & MSGFLAG_HASATTACH) { f |= MAIL_ATTACHMENTS; } if (Flags->Value.ul & MSGFLAG_READ) { f |= MAIL_READ; } To->SetFlags(f); } To->Update(); To->Save(Folder); To = NULL; return true; } bool PropToDate(SPropValue *p, LDateTime &dt) { if (p && PROP_TYPE(p->ulPropTag) == PT_SYSTIME) { uint64 i = (((uint64)p->Value.ft.dwHighDateTime) << 32) | p->Value.ft.dwLowDateTime; return dt.Set(i); } return false; } Contact *MatchContact(ScribeFolder *f, Contact *c1) { f->LoadThings(); for (auto t: f->Items) { Contact *c2 = t->IsContact(); if (c2) { #define CmpOpt(name) \ { \ const char *s1, *s2; \ if (!c1->Get(name, s1) || \ !c2->Get(name, s2) || \ _stricmp(s1, s2) != 0) \ { \ continue; \ } \ } CmpOpt(OPT_First); CmpOpt(OPT_Last); CmpOpt(OPT_Email); return c2; } } return 0; } Calendar *MatchCalendar(ScribeFolder *f, Calendar *c1) { f->LoadThings(); for (auto t: f->Items) { Calendar *c2 = t->IsCalendar(); if (c2) { LDateTime d1, d2; if (c1->GetField(FIELD_CAL_START_UTC, d1) && c2->GetField(FIELD_CAL_START_UTC, d2) && d1 != d2) { continue; } if (c1->GetField(FIELD_CAL_END_UTC, d1) && c2->GetField(FIELD_CAL_END_UTC, d2) && d1 != d2) { continue; } const char *s1, *s2; if (c1->GetField(FIELD_CAL_SUBJECT, s1) && c2->GetField(FIELD_CAL_SUBJECT, s2) && _stricmp(s1, s2) != 0) { continue; } int t1, t2; if (c1->GetField(FIELD_CAL_TYPE, t1) && c2->GetField(FIELD_CAL_TYPE, t2) && t1 != t2) { continue; } return c2; } } return 0; } bool OutlookIO::ImportItem(ScribeFolder *Out, IMAPIFolder *In, SPropValue *EntryId) { bool Status = false; if (Out && In && EntryId) { LDataStoreI::StoreTrans StoreTransaction = Out->GetObject()->GetStore()->StartTransaction(); ULONG Type = 0; IUnknown *Item = 0; HRESULT e; if ((e = In->OpenEntry( EntryId->Value.bin.cb, (LPENTRYID)EntryId->Value.bin.lpb, NULL, MAPI_BEST_ACCESS, &Type, &Item)) == S_OK && Item) { switch (Type) { case MAPI_MESSAGE: { IMessage *Msg = 0; if (Item->QueryInterface(IID_IMessage, (void**)&Msg) == S_OK && Msg) { SPropValue *Class = MapiGetProp(Msg, PR_ORIG_MESSAGE_CLASS); if (!Class) { Class = MapiGetProp(Msg, PR_MESSAGE_CLASS); } if (Class) { char *sClass = MapiCastString(Class); bool Note = _strnicmp(sClass, "IPM.Note", 8) == 0; bool Post = _strnicmp(sClass, "IPM.Post", 8) == 0; bool Document = _strnicmp(sClass, "IPM.Document", 12) == 0; if (Note || Document || Post) { // Email Mail *Email = new Mail(Out->App); if (Email) { Email->App = App; Status = ImportEmail(Out, Email, Msg, Post); } LAssert(Email == NULL); } else if (_stricmp(sClass, "IPM.Contact") == 0) { // Contact Contact *c = new Contact(Out->App); if (c) { c->App = App; #define ConvertStr(opt, tag) \ { auto s = LFromNativeCp(MapiGetPropStr(Msg, tag)); \ c->Set(opt, s); } ConvertStr(OPT_First, PR_GIVEN_NAME); ConvertStr(OPT_Last, PR_SURNAME); char *Email = 0; #define CheckTagForEmail(tag) \ if (!Email) { char *e = MapiGetPropStr(Msg, tag); \ if (e && strchr(e, '@')) Email = e; } CheckTagForEmail(0x81b0001e); CheckTagForEmail(0x805d001e); CheckTagForEmail(0x8060001e); CheckTagForEmail(0x81ae001e); c->Set(OPT_Email, Email); Contact *Match = MatchContact(Out, c); if (Match) { DeleteObj(c); c = Match; } ConvertStr(OPT_HomeStreet, PR_STREET_ADDRESS); ConvertStr(OPT_HomeSuburb, PR_LOCALITY); // city ConvertStr(OPT_HomePostcode, PR_POSTAL_CODE); ConvertStr(OPT_HomeState, PR_STATE_OR_PROVINCE); ConvertStr(OPT_HomeCountry, PR_COUNTRY); ConvertStr(OPT_WorkPhone, PR_BUSINESS_TELEPHONE_NUMBER); ConvertStr(OPT_HomePhone, PR_HOME_TELEPHONE_NUMBER); ConvertStr(OPT_HomeMobile, PR_CELLULAR_TELEPHONE_NUMBER); ConvertStr(OPT_HomeFax, PR_PRIMARY_FAX_NUMBER); char *WebPage = MapiGetPropStr(Msg, PR_PERSONAL_HOME_PAGE); if (!WebPage) WebPage = MapiGetPropStr(Msg, PR_BUSINESS_HOME_PAGE); c->Set(OPT_HomeWebPage, WebPage); ConvertStr(OPT_Nick, PR_NICKNAME); ConvertStr(OPT_Spouse, PR_SPOUSE_NAME); ConvertStr(OPT_Note, PR_BODY); c->Save(Out); Status = true; } } else if (_stricmp(sClass, "IPM.Appointment") == 0) { Calendar *c = new Calendar(Out->App); if (c) { c->App = App; bool Historical = false; LDateTime dt, Now; Now.SetNow(); SPropValue *p = MapiGetProp(Msg, PR_START_DATE); if (PropToDate(p, dt)) { Historical = dt < Now; c->SetField(FIELD_CAL_START_UTC, dt); } p = MapiGetProp(Msg, PR_END_DATE); if (PropToDate(p, dt)) { c->SetField(FIELD_CAL_END_UTC, dt); } char *s = MapiGetPropStr(Msg, PR_SUBJECT); if (s) c->SetField(FIELD_CAL_SUBJECT, s); Calendar *Match = MatchCalendar(Out, c); if (Match) { c->DecRef(); c = Match; } s = MapiGetPropStr(Msg, 0x810C001E); if (s) c->SetField(FIELD_CAL_LOCATION, s); s = MapiGetPropStr(Msg, PR_BODY); if (s) c->SetField(FIELD_CAL_NOTES, s); // Only import alarms for future objects, otherwise a replicate from // Outlook -> Scribe sets off all the old alarms. Which is annoying. if (!Historical) { p = MapiGetProp(Msg, 0x81EE000B); if (p && p->Value.b) { /* FIXME c->SetField(FIELD_CAL_REMINDER_ACTION, true); int i = MapiCastInt(MapiGetProp(Msg, 0x81E90003)); c->SetField(FIELD_CAL_REMINDER_TIME, -i); */ } } // Erm, you'd usually have to map the names here, but I used the Outlook list // to write my calendar, so it doesn't need converting... ;) int ShowAs = MapiCastInt(MapiGetProp(Msg, 0x81930003)); c->SetField(FIELD_CAL_SHOW_TIME_AS, ShowAs); c->Save(Out); Status = true; } } } Msg->Release(); } else { LgiTrace("%s:%i - QueryInterface for IMessage failed.\n", __FILE__, __LINE__); } break; } } Item->Release(); } else { LgiTrace("%s:%i - OpenEntry failed.\n", __FILE__, __LINE__); } } return Status; } int OutlookIO::FolderType(IMAPIFolder *In) { int Status = MAGIC_MAIL; LPMAPITABLE Items = 0; if (In && In->GetContentsTable(0, &Items) == S_OK) { // Loop through all the items bool Done = false; for (LMapiList Lst(Items); Lst.More() && !Done; Lst.Next()) { SPropValue *EntryId = Lst.GetField(PR_ENTRYID); if (EntryId) { ULONG Type = 0; IUnknown *Item = 0; if (In->OpenEntry( EntryId->Value.bin.cb, (LPENTRYID)EntryId->Value.bin.lpb, NULL, MAPI_BEST_ACCESS, &Type, &Item) == S_OK && Item) { IMessage *Msg = 0; if (Item->QueryInterface(IID_IMessage, (void**)&Msg) == S_OK && Msg) { char *Vs; SPropValue *v = MapiGetProp(Msg, PR_MESSAGE_CLASS); if (v && (Vs = MapiCastString(v))) { if (_stricmp(Vs, "IPM.Note") == 0) { Status = MAGIC_MAIL; Done = true; } else if (_stricmp(Vs, "IPM.Contact") == 0) { Status = MAGIC_CONTACT; Done = true; } else if (_stricmp(Vs, "IPM.Appointment") == 0) { Status = MAGIC_CALENDAR; Done = true; } } Msg->Release(); } } } } } return Status; } LString ConvertToString(LString::Array &Path) { LString d("/"); LString ret = d + d.Join(Path); return ret; } bool OutlookIO::Import( ImportParams *P, IMAPIFolder *In, ScribeFolder *Out, LString::Array &Path) { bool Status = false; if (App && Out && In) { const char *FolderName = Out->GetText(0); if (FolderName && P->Prog) { char FolderStr[256]; sprintf_s(FolderStr, sizeof(FolderStr), "Folder: '%s'", FolderName); P->Prog->SetDescription(FolderStr); } bool ImportThisFolder = P->AllFolders; if (!ImportThisFolder) { auto CurPath = ConvertToString(Path); for (auto s: P->Import) { if (_stricmp(CurPath, s) == 0) { ImportThisFolder = true; break; } } } if (ImportThisFolder) { // look through all the items for mail and contacts LPMAPITABLE Items = 0; if (In->GetContentsTable(0, &Items) == S_OK) { // ViewTable(Items, Wnd); Status = true; LMapiList Lst(Items); if (Lst.Length() > 0) { int GoodConvert = 0; if (P->ItemProg) { P->ItemProg->SetDescription("Processing items..."); P->ItemProg->SetRange(Lst.Length()); } // Loop through all the items for (; Lst.More() && !P->ItemProg->IsCancelled(); Lst.Next()) { SPropValue *v = Lst.GetField(PR_ENTRYID); if (v) { if (ImportItem(Out, In, v)) { GoodConvert++; } else { #ifdef _DEBUG // try it again ImportItem(Out, In, v); #endif } } if (P->ItemProg) { P->ItemProg->Value(Lst.Index()); } } if (P->ItemProg) { P->ItemProg->SetDescription(""); P->ItemProg->Value(0); P->ItemProg->SetRange(0); } } } } // look through all the children folders for more stuff to import LPMAPITABLE Folders = 0; if (In->GetHierarchyTable(0, &Folders) == S_OK) { // Loop through all the folders for (LMapiList Lst(Folders); Lst.More() && !P->Prog->IsCancelled(); Lst.Next()) { SPropValue *v = Lst.GetField(PR_ENTRYID); SPropValue *Name = Lst.GetField(PR_DISPLAY_NAME); if (v && Name) { IMAPIFolder *ChildIn; ULONG Type; if (In->OpenEntry(v->Value.bin.cb, (LPENTRYID)v->Value.bin.lpb, NULL, MAPI_BEST_ACCESS, &Type, (IUnknown**)&ChildIn) == S_OK && ChildIn) { // Check that this folder exists auto OutlookName = LFromNativeCp(MapiCastString(Name)); char Str[256]; auto OutPath = Out->GetPath(); strcpy_s(Str, sizeof(Str), OutPath); if (Str[strlen(Str)-1] != '/') strcat(Str, "/"); strcat(Str, OutlookName); ScribeFolder *ChildOut = App->GetFolder(Str); if (!ChildOut) { ChildOut = Out->CreateSubDirectory(OutlookName, FolderType(ChildIn)); } if (ChildOut) { Path.New() = MapiCastString(Name); Status &= Import(P, ChildIn, ChildOut, Path); ChildOut->Save(); Path.PopLast(); } ChildIn->Release(); } } } } } return Status; } void OutlookIO::ImportPersonalAddressBook(std::function callback) { auto Dlg = new FolderDlg(App, App, MAGIC_CONTACT); Dlg->DoModal([this, Dlg, callback](auto dlg, auto ctrlId) { if (!ctrlId) { delete dlg; if (callback) callback(false); return; } bool Status = false; HRESULT Error = S_OK; IAddrBook *AddrBook = 0; auto DestFolder = App->GetFolder(Dlg->Get()); if (DestFolder && (Error = Session->OpenAddressBook(Ui, NULL, 0, &AddrBook)) == S_OK && AddrBook) { ULONG EntryIDSize = 0; ENTRYID *PersonalAddrBook = 0; if (AddrBook->GetPAB(&EntryIDSize, &PersonalAddrBook) == S_OK && PersonalAddrBook) { ULONG Type = 0; IDistList *DistList = 0; if (AddrBook->OpenEntry(EntryIDSize, PersonalAddrBook, NULL, 0, &Type, (IUnknown**)&DistList) == S_OK && DistList) { int NewContacts = 0; LPMAPITABLE DistContents = 0; if (DistList->GetContentsTable(0, &DistContents) == S_OK && DistContents) { for (LMapiList Lst(DistContents); Lst.More(); Lst.Next()) { SPropValue *v = Lst.GetField(PR_ENTRYID); if (v) { ULONG type = 0; IMailUser *User = 0; if (DistList->OpenEntry( v->Value.bin.cb, (LPENTRYID) v->Value.bin.lpb, 0, 0, &type, (IUnknown**)&User) == S_OK && User) { SPropValue *Array = 0; ULONG Values = 0; if (User->GetProps( NULL, // Props 0, &Values, &Array) == S_OK && Array) { Contact *Person = (Contact*)App->CreateItem(MAGIC_CONTACT, DestFolder, false); if (Person) { for (unsigned n=0; nSet(OPT_First, Name); Person->Set(OPT_Last, Space+1); const char *Last = 0; Person->Get(OPT_Last, Last); int n=0; } else { Person->Set(OPT_First, Name); } } break; } case PR_EMAIL_ADDRESS: case PR_SMTP_ADDRESS: { char *Addr = Array[n].Value.lpszA; if (strchr(Addr, '@')) { Person->Set(OPT_Email, Array[n].Value.lpszA); } break; } default: { if (PROP_ID(Array[n].ulPropTag) >= 0x8000 && PROP_ID(Array[n].ulPropTag) <= 0x8100 && PROP_TYPE(Array[n].ulPropTag) == PT_STRING8) { char *Addr = Array[n].Value.lpszA; if (strchr(Addr, '@')) { Person->Set(OPT_Email, Array[n].Value.lpszA); } } break; } } } Person->Save(); NewContacts++; } } } } } } char Msg[256]; sprintf_s(Msg, sizeof(Msg), "%i contacts imported from Outlook.", NewContacts); LgiMsg(App, Msg, AppName, MB_OK); Status = true; } else { LgiMsg(App, "Couldn't open the personal address book.", "Error", MB_OK); } } else { LgiMsg(App, "No personal address book.", "Error", MB_OK); } } else { LgiMsg(App, "Couldn't open the address book.", "Error", MB_OK); } delete dlg; if (callback) callback(Status); }); } void Import_OutlookContacts(ScribeWnd *Parent) { #ifdef WIN32 Import_Outlook(Parent, IMP_OUTLOOK_PAB); #else ScribeFolder *Contacts = Parent->GetFolder(FOLDER_CONTACTS); LFileSelect Select; Select.Parent(Parent); Select.Type("Outlook Contacts", "*.csv"); if (Contacts && Select.Open()) { ImpRecordSet Rs; if (ReadCsv(Select.Name(), Rs) > 0) { for (ImpRecord *r = Rs.First(); r; r = Rs.Next()) { Contact *c = new Contact; if (c) { CopyField(c, OPT_First, r, "First Name"); CopyField(c, OPT_Last, r, "Last Name"); CopyField(c, OPT_Email, r, "E-mail Address"); CopyField(c, OPT_Street, r, "Home Street"); CopyField(c, OPT_Suburb, r, "Home City"); CopyField(c, OPT_State, r, "Home State"); CopyField(c, OPT_Postcode, r, "Home Postal Code"); CopyField(c, OPT_Country, r, "Home Country"); CopyField(c, OPT_Work, r, "Business Phone"); CopyField(c, OPT_Home, r, "Home Phone"); CopyField(c, OPT_Mobile, r, "Mobile Phone"); CopyField(c, OPT_Fax, r, "Business Fax"); CopyField(c, OPT_WebPage, r, "Web Page"); c->Save(Contacts); } } } } #endif } //////////////////////////////////////////////////////////////////////////// bool SetPropToEntryId(SPropValue &Prop, int Id, char *Email, char *Name) { /* struct SenderEntry { char Flags[4]; // 0 char Muid[16]; // MAPI_ONE_OFF_UID uint16 wVersion; // 0 uint16 wFlags; // MAPI_ONE_OFF_NO_RICH_INFO (1) char *DisplayName; char *AddrType; char *EmailAddr; }; */ uchar MapiOneOffUid[] = MAPI_ONE_OFF_UID; char AddrType[] = "SMTP"; if (Email && Name) { LMemQueue p; int32 i32 = 0; int16 i16 = 0; p.Write(&i32, sizeof(i32)); p.Write(MapiOneOffUid, sizeof(MapiOneOffUid)); p.Write(&i16, sizeof(i16)); i16 = MAPI_ONE_OFF_NO_RICH_INFO; p.Write(&i16, sizeof(i16)); #define WriteStr(s) \ p.Write(s ? s : "", (s ? strlen(s) : 0) + 1); if (Name) { WriteStr(Name); } else if (Email) { WriteStr(Email); } WriteStr(AddrType); WriteStr(Email); Prop.ulPropTag = Id; Prop.Value.bin.cb = (ULONG) p.GetSize(); Prop.Value.bin.lpb = (uchar*)p.New(); return Prop.Value.bin.lpb != 0; } return false; }; int OutlookIO::CountFolders(ScribeFolder *f) { int Count = 1; for (ScribeFolder *c = f->GetChildFolder(); c; c = c->GetNextFolder()) { Count += CountFolders(c); } return Count; } bool OutlookIO::ExportCalendar(Calendar *c, IMessage *m) { bool Status = false; if (c && m) { LDateTime dt; const char *s; int i; if (c->GetCalType() == CalEvent) { MapiSetPropStr(m, PR_MESSAGE_CLASS, "IPM.Appointment", false); } else if (c->GetCalType() == CalTodo) { MapiSetPropStr(m, PR_MESSAGE_CLASS, "IPM.Task", false); } char Tz[64]; int TzOff = dt.SystemTimeZone(); sprintf_s(Tz, sizeof(Tz), "(+%.2f)", (double)TzOff / 60); MapiSetPropStr(m, 0x8091001E, Tz); if (c->GetField(FIELD_CAL_START_UTC, dt)) { MapiSetPropDate(m, PR_START_DATE, dt); MapiSetPropDate(m, 0x80800040, dt); } if (c->GetField(FIELD_CAL_END_UTC, dt)) { MapiSetPropDate(m, PR_END_DATE, dt); MapiSetPropDate(m, 0x80810040, dt); } if (c->GetField(FIELD_CAL_SUBJECT, s)) MapiSetPropStr(m, PR_SUBJECT, s); if (c->GetField(FIELD_CAL_LOCATION, s)) MapiSetPropStr(m, 0x810C001E, s); if (c->GetField(FIELD_CAL_NOTES, s)) MapiSetPropStr(m, PR_BODY, s); /* FIXME if (c->GetField(FIELD_CAL_REMINDER_ACTION, i) && i) { MapiSetPropBool(m, 0x81EE000B, true); if (c->GetField(FIELD_CAL_REMINDER_TIME, i)) MapiSetPropLong(m, 0x81E90003, -i); } */ if (c->GetField(FIELD_CAL_SHOW_TIME_AS, i)) MapiSetPropLong(m, 0x81930003, i); MapiSetPropLong(m, PR_MESSAGE_FLAGS, 1); /* MapiSetPropLong(m, 0x80030003, 369); MapiSetPropLong(m, 0x807D0003, 2); MapiSetPropBool(m, 0x8001000B, true); MapiSetPropBool(m, 0x8002000B, false); */ } return Status; } bool OutlookIO::Export( ExportParams *P, ScribeFolder *In, IMAPIFolder *Out, int Depth) { bool Status = false; HRESULT Err; // 'In' is created/found as a sub-folder of 'Out' if (App && Out && In && !P->Exclude.HasItem(In) && (P->AllFolders || P->Include.HasItem(In)) && (!P->Prog || !P->Prog->IsCancelled())) { // Create/find the output folder LPMAPITABLE Folders = 0; if ((Err = Out->GetHierarchyTable(0, &Folders)) == S_OK) { // ViewTable(Folders, App); IMAPIFolder *Match = 0; auto InName = In->GetName(true); // Loop through all the folders { for (LMapiList Lst(Folders); Lst.More() && !P->Prog->IsCancelled(); Lst.Next()) { SPropValue *Name = Lst.GetField(PR_DISPLAY_NAME); auto n = LFromNativeCp(MapiCastString(Name)); if (n && InName && _stricmp(n, InName) == 0) { SPropValue *v = Lst.GetField(PR_ENTRYID); if (v) { ULONG Type; if (Out->OpenEntry( v->Value.bin.cb, (LPENTRYID)v->Value.bin.lpb, NULL, MAPI_BEST_ACCESS, &Type, (IUnknown**)&Match) == S_OK) { break; } } } } } if (!Match) { Out->CreateFolder(FOLDER_GENERIC, InName, 0, 0, OPEN_IF_EXISTS, &Match); } if (Match) { auto FolderPath = In->GetPath(); if (P->AllFolders || P->Export.HasItem(In)) { if (P->Prog) { char s[256]; sprintf_s(s, sizeof(s), "Loading %s...", FolderPath ? FolderPath.Get() : InName.Get()); P->Prog->SetDescription(s); } // Export all the contained items, by first scanning existing entries so // that we can skip existing copies of the items in Scribe LPMAPITABLE Items = 0; LHashTbl,bool> MsgIds; if ((Err = Match->GetContentsTable(0, &Items)) == S_OK) { for (LMapiList Lst(Items); Lst.More(); Lst.Next()) { SPropValue *v = Lst.GetField(PR_ENTRYID); if (v) { ULONG Type = 0; IUnknown *Item = 0; if ((Err = Match->OpenEntry(v->Value.bin.cb, (LPENTRYID)v->Value.bin.lpb, NULL, MAPI_BEST_ACCESS, &Type, &Item)) == S_OK && Item) { if (Type == MAPI_MESSAGE) { IMessage *Msg = 0; if (Item->QueryInterface(IID_IMessage, (void**)&Msg) == S_OK && Msg) { char *MsgId = MapiGetPropStr(Msg, PR_INTERNET_MESSAGE_ID); if (MsgId) { MsgIds.Add(MsgId, true); } Msg->Release(); } } Item->Release(); } else { LgiTrace("%s:%i - Match->OpenEntry failed, Err=%x\n", __FILE__, __LINE__, Err); } } } } // Copy items from Scribe to the MAPI store bool WasLoaded = In->IsLoaded(); In->LoadThings(); if (P->Prog) { char s[256]; sprintf_s(s, sizeof(s), "Exporting %s...", FolderPath ? FolderPath.Get() : InName.Get()); P->Prog->SetDescription(s); } if (P->ItemProg) { P->ItemProg->Value(0); P->ItemProg->SetRange(In->Items.Length()); P->ItemProg->SetType("email"); P->ItemProg->Cancel(false); } for (auto t: In->Items) { if (P->ItemProg && P->ItemProg->IsCancelled()) break; Mail *m = t->IsMail(); if (m) { char MsgId[256]; sprintf_s(MsgId, sizeof(MsgId), "<%s>", m->GetMessageId(true)); if (!MsgIds.Find(MsgId)) { IMessage *Msg; if ((Err = Match->CreateMessage(0, 0, &Msg)) == S_OK) { SPropValue v; Msg->SetProps(1, &v, 0); MapiSetPropLong(Msg, PR_MESSAGE_FLAGS, m->GetFlags() & MAIL_READ ? MSGFLAG_READ : 0); MapiSetPropLong(Msg, PR_OBJECT_TYPE, MAPI_MESSAGE); MapiSetPropStr(Msg, PR_MESSAGE_CLASS, "IPM.Note", false); MapiSetPropStr(Msg, PR_INTERNET_MESSAGE_ID, MsgId, false); if (m->GetSubject()) { MapiSetPropStr(Msg, PR_SUBJECT, m->GetSubject(), true); } // Write the sender... if (m->GetFrom()) { SPropValue Prop; if (SetPropToEntryId(Prop, PR_SENDER_ENTRYID, (char*)m->GetFromStr(FIELD_EMAIL), (char*)m->GetFromStr(FIELD_NAME))) { Msg->SetProps(1, &Prop, 0); DeleteArray(Prop.Value.bin.lpb); } if (m->GetFromStr(FIELD_EMAIL)) { MapiSetPropStr(Msg, PR_SENDER_ADDRTYPE, "SMTP", false); MapiSetPropStr(Msg, PR_SENDER_EMAIL_ADDRESS, m->GetFromStr(FIELD_EMAIL), false); MapiSetPropStr(Msg, PR_SENT_REPRESENTING_ADDRTYPE, "SMTP", false); MapiSetPropStr(Msg, PR_SENT_REPRESENTING_EMAIL_ADDRESS, m->GetFromStr(FIELD_EMAIL), false); } auto Name = m->GetFromStr(FIELD_EMAIL) ? m->GetFromStr(FIELD_EMAIL) : m->GetFromStr(FIELD_EMAIL); if (Name) { MapiSetPropStr(Msg, PR_SENDER_NAME, Name, true); MapiSetPropStr(Msg, PR_SENT_REPRESENTING_NAME, Name, true); } } // Write out the recipients table if (m->GetTo()->Length()) { ADRLIST *Adr = 0; if (MAPIAllocateBuffer(CbNewADRLIST((ULONG)m->GetTo()->Length()), (void**) &Adr) == S_OK) { Adr->cEntries = (ULONG)m->GetTo()->Length(); int n = 0; for (LDataPropI *a = m->GetTo()->First(); a; a = m->GetTo()->Next(), n++) { Adr->aEntries[n].cValues = 3 + (a->GetStr(FIELD_EMAIL) ? 1 : 0) + (a->GetStr(FIELD_NAME) ? 1 : 0); Adr->aEntries[n].ulReserved1 = 0; if (MAPIAllocateBuffer( sizeof(SPropValue) * Adr->aEntries[n].cValues, (void**) &Adr->aEntries[n].rgPropVals) == S_OK) { SPropValue *p = Adr->aEntries[n].rgPropVals; int i = 0; if (SetPropToEntryId(p[0], PR_ENTRYID, (char*)a->GetStr(FIELD_EMAIL), (char*)a->GetStr(FIELD_NAME))) { i++; } else { Adr->aEntries[n].cValues--; } switch (a->GetInt(FIELD_CC)) { default: case MAIL_ADDR_TO: p[i].Value.l = MAPI_TO; break; case MAIL_ADDR_CC: p[i].Value.l = MAPI_CC; break; case MAIL_ADDR_BCC: p[i].Value.l = MAPI_BCC; break; } p[i].ulPropTag = PR_RECIPIENT_TYPE; p[i++].dwAlignPad = 0; p[i].ulPropTag = PR_ADDRTYPE; p[i].dwAlignPad = 0; p[i++].Value.lpszA = "SMTP"; p[i].ulPropTag = PR_DISPLAY_NAME; p[i].Value.lpszA = (char*) (a->GetStr(FIELD_NAME) ? a->GetStr(FIELD_NAME) : a->GetStr(FIELD_EMAIL)); p[i++].dwAlignPad = 0; if (a->GetStr(FIELD_EMAIL)) { p[i].ulPropTag = PR_EMAIL_ADDRESS; p[i].Value.lpszA = (char*) a->GetStr(FIELD_EMAIL); p[i++].dwAlignPad = 0; } } } HRESULT e; if ((e = Msg->ModifyRecipients(MODRECIP_ADD, Adr)) != S_OK) { // _asm int 3 } for (unsigned i=0; icEntries; i++) { MAPIFreeBuffer(Adr->aEntries[i].rgPropVals); } MAPIFreeBuffer(Adr); } } if (m->GetBody()) { LAutoString cs = m->GetCharSet(); if (cs) { char *u8 = (char*) LNewConvertCp("utf-8", m->GetBody(), cs); MapiSetPropStr(Msg, PR_BODY, u8, true); DeleteArray(u8); } else { MapiSetPropStr(Msg, PR_BODY, m->GetBody(), false); } } if (m->GetHtml()) { MapiSetPropStr(Msg, PR_BODY_HTML, m->GetHtml(), true); } if (m->GetInternetHeader()) { MapiSetPropStr(Msg, PR_TRANSPORT_MESSAGE_HEADERS, m->GetInternetHeader(), false); } MapiSetPropDate(Msg, PR_CLIENT_SUBMIT_TIME, *m->GetDateSent()); MapiSetPropDate(Msg, PR_MESSAGE_DELIVERY_TIME, *m->GetDateReceived()); // Write out any attachments List Att; if (m->GetAttachments(&Att)) { for (auto a: Att) { char *Ptr; ssize_t Size; if (a->Get(&Ptr, &Size)) { ULONG AttachmentNum = 0; LPATTACH Attach = 0; if ((Err = Msg->CreateAttach(0, 0, &AttachmentNum, &Attach)) == S_OK) { MapiSetPropLong(Attach, PR_ATTACH_METHOD, ATTACH_BY_VALUE); if (a->GetName()) { MapiSetPropStr(Attach, PR_ATTACH_LONG_FILENAME, a->GetName(), false); auto d = a->GetName(); const char *Dir = 0; while (*d) { if (*d == '/' || *d == '\\') { Dir = d; } d++; } if (Dir) { Dir++; } else { Dir = a->GetName(); } MapiSetPropStr(Attach, PR_DISPLAY_NAME, Dir, false); } if (a->GetMimeType()) { MapiSetPropStr(Attach, PR_ATTACH_MIME_TAG, a->GetMimeType(), false); } if (a->GetContentId()) { MapiSetPropStr(Attach, PR_ATTACH_CONTENT_ID, a->GetContentId(), false); } SPropValue Bin; Bin.dwAlignPad = 0; Bin.ulPropTag = PR_ATTACH_DATA_BIN; Bin.Value.bin.cb = (ULONG) Size; Bin.Value.bin.lpb = (uchar*)Ptr; Attach->SetProps(1, &Bin, 0); Attach->SaveChanges(0); Attach->Release(); } else { LgiTrace("%s:%i - Msg->CreateAttach failed, Err=%x\n", __FILE__, __LINE__, Err); } } } } Msg->SaveChanges(0); Msg->Release(); } else { LgiTrace("%s:%i - Match->CreateMessage failed, Err=%x\n", __FILE__, __LINE__, Err); } } } Calendar *Cal = t->IsCalendar(); if (Cal) { if (Cal->GetCalType() == CalEvent) { IMessage *Msg; if ((Err = Match->CreateMessage(0, 0, &Msg)) == S_OK) { ExportCalendar(Cal, Msg); Msg->SaveChanges(0); Msg->Release(); } } else { // Find todo folder... } } if (P->ItemProg) P->ItemProg->Value(P->ItemProg->Value()+1); } if (P->Prog) P->Prog->Value(P->Prog->Value()+1); if (!WasLoaded) { In->UnloadThings(); } } // Export child folders... for (ScribeFolder *c = In->GetChildFolder(); c; c = c->GetNextFolder()) { Export(P, c, Match, Depth + 1); } Match->Release(); } } else { LgiTrace("%s:%i - Out->GetHierarchyTable failed, Err=%x\n", _FL, Err); } } return Status; } ////////////////////////////////////////////////////////////////////////////// class MailMapiSource : public MailSource { protected: OutlookIO *Mapi; ScribeWnd *Parent; ScribeAccount *Account; UI_TYPE Ui; IMsgStore *MsgStore; IMAPIFolder *pFolder; List Entries; bool ListEntries(); bool OnMsgStore(HRESULT Result); public: MailMapiSource(ScribeWnd *parent, ScribeAccount *account); ~MailMapiSource(); // Connection bool Open(LSocketI *S, const char *RemoteHost, int Port, const char *User, const char *Password, LDom *SettingStore, int Flags = 0); bool Close(); // Commands available while connected ssize_t GetMessages() override; bool Receive(LArray &Trans, MailCallbacks *Callbacks); bool Delete(int Message); int Sizeof(int Message); bool GetUid(int Message, char *Id, int IdLen); bool GetUidList(LString::Array &Id); LString GetHeaders(int Message); }; MailMapiSource::MailMapiSource(ScribeWnd *parent, ScribeAccount *account) { MsgStore = 0; pFolder = 0; Parent = parent; Account = account; Ui = (UI_TYPE) ((Parent) ? Parent->Handle() : 0); Mapi = new OutlookIO(Parent, 0, Account); } MailMapiSource::~MailMapiSource() { if (pFolder) { pFolder->Release(); } if (MsgStore) { MsgStore->Release(); } DeleteObj(Mapi); } bool MailMapiSource::OnMsgStore(HRESULT Result) { if (FAILED(Result)) return false; if (!MsgStore) return false; SPropValue *SubTree = MapiGetProp(MsgStore, PR_IPM_SUBTREE_ENTRYID); if (!SubTree) return false; ULONG ObjType; IMAPIFolder *Root = 0; if (MsgStore->OpenEntry(SubTree->Value.bin.cb, (LPENTRYID)SubTree->Value.bin.lpb, NULL, MAPI_BEST_ACCESS, &ObjType, (IUnknown**) &Root) != S_OK || !Root) return false; LPMAPITABLE Folders = 0; if (Root->GetHierarchyTable(0, &Folders) != S_OK) return false; // Loop through all the folders bool Status = false; for (LMapiList Lst(Folders); Lst.More(); Lst.Next()) { SPropValue *v = Lst.GetField(PR_ENTRYID); SPropValue *Name = Lst.GetField(PR_DISPLAY_NAME); if (v && Name) { char *FolderName = MapiCastString(Name); if (FolderName && _stricmp(FolderName, "Inbox") == 0) { ULONG Type; Status |= Root->OpenEntry(v->Value.bin.cb, (LPENTRYID)v->Value.bin.lpb, NULL, MAPI_BEST_ACCESS, &Type, (IUnknown**)&pFolder) == S_OK && pFolder; } } } return Status; } bool MailMapiSource::Open(LSocketI *S, const char *RemoteHost, int Port, const char *User, const char *Password, LDom *SettingStore, int Flags) { bool Status = true; if (Account && Mapi) { LVariant Server = Account->Receive.Server(); if (Server.Str()) { char PassStr[128] = ""; GPassword p; Account->Receive.GetPassword(&p); p.Get(PassStr); Mapi->MAPILogonEx( Ui, Server.Str(), ValidStr(PassStr) ? PassStr : 0, MAPI_EXTENDED, Mapi->Session.Set()); } if (!Status) { Mapi->MAPILogonEx( Ui, NULL, NULL, MAPI_LOGON_UI | MAPI_EXTENDED, Mapi->Session.Set()); if (Account && Mapi->Session) { // when we're called with a blank account we should be nice and // record what the user entered for their profile LPMAPITABLE Tbl = 0; if (Mapi->Session->GetStatusTable(0, &Tbl) == SUCCESS_SUCCESS) { // ViewTable(Tbl, Window); for (LMapiList Lst(Tbl); Lst.More(); Lst.Next()) { SPropValue *Type = Lst.GetField(PR_RESOURCE_TYPE); SPropValue *Name = Lst.GetField(PR_DISPLAY_NAME); if (MapiCastInt(Type) == MAPI_SUBSYSTEM) { // this should be the profile name Account->Receive.Server(MapiCastString(Name)); break; } } } } } if (Mapi->Session) { LVariant UserName = Account->Receive.UserName(); if (!ValidStr(UserName.Str())) { auto Dlg = new ChooseMessageStoreDlg(Parent, Mapi->Session, false); if (!Dlg->InitCheck()) { delete Dlg; } else { Dlg->DoModal([this, Dlg](auto dlg, auto id) { if (id && Dlg->Ref) { Account->Receive.UserName(Dlg->Ref->DisplayName); auto Error = Mapi->Session->OpenMsgStore(Ui, Dlg->Ref->Size, // entry bytes Dlg->Ref->Entry, // ptr to entry NULL, // default interface: IMsgStore MAPI_BEST_ACCESS, &MsgStore); OnMsgStore(Error); } delete dlg; }); } } else { MessageStores Entries(Mapi->Session); if (Entries.Status) { for (auto e: Entries) { if (e->DisplayName && _stricmp(e->DisplayName, UserName.Str()) == 0) { auto Error = Mapi->Session->OpenMsgStore(Ui, e->Size, // entry bytes e->Entry, // ptr to entry NULL, // default interface: IMsgStore MAPI_BEST_ACCESS, &MsgStore); if (OnMsgStore(Error)) break; } } } } } } return Status; } bool MailMapiSource::Close() { if (Mapi && Mapi->Session) { return true; } return false; } bool MailMapiSource::ListEntries() { if (pFolder && Entries.Length() < 1) { LPMAPITABLE Items = 0; if (pFolder->GetContentsTable(0, &Items) == S_OK) { for (LMapiList Lst(Items); Lst.More(); Lst.Next()) { SPropValue *v = Lst.GetField(PR_ENTRYID); if (v) { Entries.Insert(new MapiEntry(v)); } } } } return Entries.Length() > 0; } ssize_t MailMapiSource::GetMessages() { ssize_t Status = 0; if (ListEntries()) { Status = Entries.Length(); } return Status; } bool MailMapiSource::Receive(LArray &Trans, MailCallbacks *Callbacks) { bool Status = false; for (unsigned n=0; nIndex); if (e && Trans[n]->Stream) { ULONG Type = 0; IUnknown *Item = 0; if (pFolder->OpenEntry( e->Len, (LPENTRYID)e->Bin, NULL, MAPI_BEST_ACCESS, &Type, &Item) == S_OK && Item) { switch (Type) { case MAPI_MESSAGE: { IMessage *IMsg = 0; if (Item->QueryInterface(IID_IMessage, (void**)&IMsg) == S_OK) { SPropValue *Class = MapiGetProp(IMsg, PR_ORIG_MESSAGE_CLASS); if (!Class) { Class = MapiGetProp(IMsg, PR_MESSAGE_CLASS); } if (Class) { char *sClass = MapiCastString(Class); bool Note = _stricmp(sClass, "IPM.Note") == 0; bool Post = _stricmp(sClass, "IPM.Post") == 0; bool Document = _strnicmp(sClass, "IPM.Document", 12) == 0; if (Note || Post || Document) { // FIXME // Status |= Trans[n]->Status = Mapi->ImportEmail(dynamic_cast(Msg), IMsg, Post); } } IMsg->Release(); } break; } } Item->Release(); } } } return Status; } bool MailMapiSource::Delete(int Message) { return false; } int MailMapiSource::Sizeof(int Message) { return 0; } bool MailMapiSource::GetUid(int Message, char *Id, int IdLen) { return false; } bool MailMapiSource::GetUidList(LString::Array &Id) { return false; } LString MailMapiSource::GetHeaders(int Message) { return NULL; } MailSource *NewOutlookMailSource(ScribeWnd *Parent, ScribeAccount *Account) { return new MailMapiSource(Parent, Account); } diff --git a/Code/Imp_Eudora.cpp b/Code/Imp_Eudora.cpp --- a/Code/Imp_Eudora.cpp +++ b/Code/Imp_Eudora.cpp @@ -1,177 +1,180 @@ #include "Scribe.h" #include "resdefs.h" #include "lgi/common/LgiRes.h" char EudoraPathKey[] = "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\Eudora.exe"; bool ImportEudoraAddresss(ScribeWnd *App, ScribeFolder *Folder, char *File) { - bool Status = false; + if (!App || !Folder || !File) + return false; + + LFile f(File); + if (!f) + return false; - if (App && Folder && File) + auto Text = f.Read(); + if (!Text) + return false; + + bool Status = false; + char *Alias = 0; + char *Address = 0; + + auto L = Text.SplitDelimit("\r\n"); + for (unsigned i=0; i 2) { - char *Alias = 0; - char *Address = 0; - - LToken L(Text, "\r\n"); - for (unsigned i=0; i 2) + DeleteArray(Alias); + Alias = NewStr(S[1]); + DeleteArray(Address); + Address = NewStr(S[2]); + } + else if (_stricmp(S[0], "note") == 0) + { + if (Alias && Address) { - if (_stricmp(S[0], "alias") == 0) + Contact *c = new Contact(App); + if (c) { - DeleteArray(Alias); - Alias = NewStr(S[1]); - DeleteArray(Address); - Address = NewStr(S[2]); - } - else if (_stricmp(S[0], "note") == 0) - { - if (Alias && Address) - { - Contact *c = new Contact(App); - if (c) - { - c->App = App; + c->App = App; - // set basic information - c->Set(OPT_Nick, Alias); - c->Set(OPT_Email, Address); - - // parse through all the tags.. - char *n = 0; - for (char *s = strchr(L[i], '<'); s && *s; s = n) - { - char *Var = ++s; - n = strchr(Var, '>'); - if (n) - { - *n++ = 0; + // set basic information + c->Set(OPT_Nick, Alias); + c->Set(OPT_Email, Address); - char *Val = strchr(Var, ':'); - if (Val) - { - *Val++ = 0; + // parse through all the tags.. + char *n = 0; + for (char *s = strchr(L[i], '<'); s && *s; s = n) + { + char *Var = ++s; + n = strchr(Var, '>'); + if (n) + { + *n++ = 0; + + char *Val = strchr(Var, ':'); + if (Val) + { + *Val++ = 0; - #define Map(From, To) else if (_stricmp(Var, From) == 0) c->Set(To, Val) - if (_stricmp(Var, "first") == 0) - { - c->Set(OPT_First, Val); - } - Map("last", OPT_Last); - Map("address", OPT_HomeStreet); - Map("city", OPT_HomeSuburb); - Map("state", OPT_HomeState); - Map("country", OPT_HomeCountry); - Map("zip", OPT_HomePostcode); - Map("phone", OPT_HomePhone); - Map("fax", OPT_HomeFax); - Map("mobile", OPT_HomeMobile); - Map("web", OPT_HomeWebPage); - // Map("title", ); - Map("company", OPT_Company); - Map("address2", OPT_WorkStreet); - Map("city2", OPT_WorkSuburb); - Map("country2", OPT_WorkCountry); - Map("zip2", OPT_WorkPostcode); - Map("phone2", OPT_WorkPhone); - Map("fax2", OPT_WorkFax); - Map("web2", OPT_WorkWebPage); - else if (_stricmp(Var, "name") == 0) - { - char *Sp = strchr(Val, ' '); - if (Sp) - { - *Sp++ = 0; - c->Set(OPT_First, Val); - c->Set(OPT_Last, Sp); - } - else - { - c->Set(OPT_First, Val); - } - } - else if (_stricmp(Var, "otheremail") == 0) - { - - } + #define Map(From, To) else if (_stricmp(Var, From) == 0) c->Set(To, Val) + if (_stricmp(Var, "first") == 0) + { + c->Set(OPT_First, Val); + } + Map("last", OPT_Last); + Map("address", OPT_HomeStreet); + Map("city", OPT_HomeSuburb); + Map("state", OPT_HomeState); + Map("country", OPT_HomeCountry); + Map("zip", OPT_HomePostcode); + Map("phone", OPT_HomePhone); + Map("fax", OPT_HomeFax); + Map("mobile", OPT_HomeMobile); + Map("web", OPT_HomeWebPage); + // Map("title", ); + Map("company", OPT_Company); + Map("address2", OPT_WorkStreet); + Map("city2", OPT_WorkSuburb); + Map("country2", OPT_WorkCountry); + Map("zip2", OPT_WorkPostcode); + Map("phone2", OPT_WorkPhone); + Map("fax2", OPT_WorkFax); + Map("web2", OPT_WorkWebPage); + else if (_stricmp(Var, "name") == 0) + { + char *Sp = strchr(Val, ' '); + if (Sp) + { + *Sp++ = 0; + c->Set(OPT_First, Val); + c->Set(OPT_Last, Sp); + } + else + { + c->Set(OPT_First, Val); } } - - char *Next = strchr(n, '<'); - if (!Next && strlen(n) > 0) + else if (_stricmp(Var, "otheremail") == 0) { - c->Set(OPT_Note, n); - n = 0; + } - else n = Next; } + } - Status |= (Folder->WriteThing(c, NULL) != Store3Error); + char *Next = strchr(n, '<'); + if (!Next && strlen(n) > 0) + { + c->Set(OPT_Note, n); + n = 0; } + else n = Next; } + + Status |= (Folder->WriteThing(c, NULL) != Store3Error); } } } } } return Status; } void Import_EudoraAddressBook(ScribeWnd *App) { char Str[256] = "/"; #ifdef WIN32 HKEY hKey; if (RegOpenKeyA(HKEY_LOCAL_MACHINE, EudoraPathKey, &hKey) == ERROR_SUCCESS) { DWORD Type = 0; DWORD Size = sizeof(Str); RegQueryValueExA(hKey, "Path", 0, &Type, (uchar*)Str, &Size); RegCloseKey(hKey); } #endif LString::Array Files; LMakePath(Str, sizeof(Str), Str, "NNdbase.txt"); if (LFileExists(Str)) { Files.Add(Str); } // Get default path.. char DefaultFolder[256] = "/Contacts"; ScribeFolder *f = App->GetCurrentFolder(); if (!f || f->GetItemType() != MAGIC_CONTACT) { f = App->GetFolder(FOLDER_CONTACTS); } if (f && f->GetItemType() == MAGIC_CONTACT) { auto p = f->GetPath(); if (p) { strcpy_s(DefaultFolder, sizeof(DefaultFolder), p); } } // Ask user... auto Dlg = new ChooseFolderDlg(App, false, "Eudora", LLoadString(IDS_SELECT_IO), DefaultFolder, MAGIC_CONTACT, &Files); Dlg->DoModal([App, Dlg](auto dlg, auto id) { if (id && Dlg->SrcFiles[0]) ImportEudoraAddresss(App, App->GetFolder(Dlg->DestFolder), Dlg->SrcFiles[0]); delete dlg; }); } diff --git a/Code/Resource.rc b/Code/Resource.rc --- a/Code/Resource.rc +++ b/Code/Resource.rc @@ -1,139 +1,139 @@ // Microsoft Visual C++ generated resource script. // #include "resource.h" #define APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 2 resource. // #include "winres.h" ///////////////////////////////////////////////////////////////////////////// #undef APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // English (U.S.) resources #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) #ifdef _WIN32 LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US #pragma code_page(1252) #endif //_WIN32 ///////////////////////////////////////////////////////////////////////////// // // Icon // // Icon with lowest ID value placed first to ensure application icon // remains consistent on all systems. IDI_APP ICON "..\\Resources\\Icons\\app.ico" IDI_MAIL ICON "..\\Resources\\Icons\\mail.ico" IDI_BLANK ICON "..\\Resources\\Icons\\blank.ico" IDI_SMALL ICON "..\\Resources\\Icons\\small.ico" IDI_CONTACT ICON "..\\Resources\\Icons\\contact.ico" IDI_FILTER ICON "..\\Resources\\Icons\\filter.ico" IDI_CALENDER ICON "..\\Resources\\Icons\\calender.ico" IDI_EVENT ICON "..\\Resources\\Icons\\event.ico" #endif // English (U.S.) resources ///////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////// // English (Australia) resources #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENA) #ifdef _WIN32 LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_AUS #pragma code_page(1252) #endif //_WIN32 ///////////////////////////////////////////////////////////////////////////// // // Icon // // Icon with lowest ID value placed first to ensure application icon // remains consistent on all systems. IDI_ERR ICON "..\\Resources\\Icons\\error.ico" #ifdef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // TEXTINCLUDE // 1 TEXTINCLUDE BEGIN "resource.h\0" END 2 TEXTINCLUDE BEGIN "#include ""afxres.h""\r\n" "\0" END 3 TEXTINCLUDE BEGIN "\r\n" "\0" END #endif // APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // Version // VS_VERSION_INFO VERSIONINFO - FILEVERSION 3,1,128 - PRODUCTVERSION 3,1,128 + FILEVERSION 3,1,135 + PRODUCTVERSION 3,1,135 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS 0x1L #else FILEFLAGS 0x0L #endif FILEOS 0x40004L FILETYPE 0x1L FILESUBTYPE 0x0L BEGIN BLOCK "StringFileInfo" BEGIN BLOCK "0c0904b0" BEGIN VALUE "CompanyName", "Memecode" VALUE "FileDescription", "Scribe" - VALUE "FileVersion", "3,1,128\0" + VALUE "FileVersion", "3,1,135\0" VALUE "InternalName", "Scribe" VALUE "LegalCopyright", "Copyright © 2016" VALUE "OriginalFilename", "Scribe.exe" VALUE "ProductName", "i.Scribe\0" - VALUE "ProductVersion", "3,1,128\0" + VALUE "ProductVersion", "3,1,135\0" END END BLOCK "VarFileInfo" BEGIN VALUE "Translation", 0xc09, 1200 END END #endif // English (Australia) resources ///////////////////////////////////////////////////////////////////////////// #ifndef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 3 resource. // ///////////////////////////////////////////////////////////////////////////// #endif // not APSTUDIO_INVOKED diff --git a/Code/Scribe.h b/Code/Scribe.h --- a/Code/Scribe.h +++ b/Code/Scribe.h @@ -1,2569 +1,2569 @@ /*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 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; } bool GetDirty() { return Dirty; } 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 SetDateField(int Field, LVariant &v); 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 GetDateField(int Field, LVariant &v); 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 SetVariant(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 LString::Array 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 int GetFolderVersion(const char *Path); extern bool CreateMailHeaders(ScribeWnd *App, LStream &Out, LDataI *Mail, MailProtocol *Protocol); extern void Base36(char *Out, uint64 In); //////////////////////////////////////////////////////////// // 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(LString::Array &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; + LString 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/ScribeAccount.cpp b/Code/ScribeAccount.cpp --- a/Code/ScribeAccount.cpp +++ b/Code/ScribeAccount.cpp @@ -1,567 +1,567 @@ /* ** FILE: ScribeAccount.cpp ** AUTHOR: Matthew Allen ** DATE: 19/10/99 ** DESCRIPTION: Scribe account ** ** Copyright (C) 2002, Matthew Allen ** fret@memecode.com */ // Includes #include #include #include #include #include "Scribe.h" #include "lgi/common/Edit.h" #include "ScribeAccountUI.h" #include "../Resources/resdefs.h" #include "lgi/common/LgiRes.h" #include "ScribeFolderSelect.h" //////////////////////////////////////////////////////////////////////////// class ScribeAccountPrivate { public: // Data int Index; LMenuItem *IdentityItem; ScribeAccountPrivate(ScribeWnd *App, int i) { IdentityItem = 0; Index = i; } }; //////////////////////////////////////////////////////////////////////////// ScribeAccount::ScribeAccount(ScribeWnd *App, int Index) : d(new ScribeAccountPrivate(App, Index)), Parent(App), Identity(this), Send(this), Receive(this) { CreateMaps(); } ScribeAccount::~ScribeAccount() { Receive.OnBeforeDelete(); Send.OnBeforeDelete(); Identity.OnBeforeDelete(); DeleteObj(d); } bool ScribeAccount::GetVariant(const char *Name, LVariant &Value, const char *Array) { ScribeDomType f = StrToDom(Name); char k[128]; LOptionsFile *Opts = GetApp()->GetOptions(); switch (f) { case SdIdentity: // Type: Accountlet.AccountIdentity Value = (LDom*)&Identity; break; case SdSend: // Type: Accountlet.SendAccountlet Value = (LDom*)&Send; break; case SdReceive: // Type: Accountlet.ReceiveAccountlet Value = (LDom*)&Receive; break; case SdIsValid: // Type: Bool Value = IsValid(); break; case SdIsOnline: // Type: Bool Value = IsOnline(); break; case SdScribe: // Type: ScribeWnd Value = (LDom*)Parent; break; case SdName: // Type: String return Opts->GetValue(Identity.OptionName(OPT_AccountName, k, sizeof(k)), Value); case SdId: // Type: String return Opts->GetValue(Identity.OptionName(OPT_AccountUID, k, sizeof(k)), Value); case SdDisable: // Type: Bool return Opts->GetValue(Identity.OptionName(OPT_AccountDisabled, k, sizeof(k)), Value); case SdAccountExpanded: // Type: Bool return Opts->GetValue(Identity.OptionName(OPT_AccountExpanded, k, sizeof(k)), Value); default: return false; } return true; } bool ScribeAccount::SetVariant(const char *Name, LVariant &Value, const char *Array) { ScribeDomType f = StrToDom(Name); char k[128]; LOptionsFile *Opts = GetApp()->GetOptions(); switch (f) { case SdName: // Type: String return Opts->SetValue(Identity.OptionName(OPT_AccountName, k, sizeof(k)), Value); case SdId: // Type: String return Opts->SetValue(Identity.OptionName(OPT_AccountUID, k, sizeof(k)), Value); case SdDisable: // Type: Bool return Opts->SetValue(Identity.OptionName(OPT_AccountDisabled, k, sizeof(k)), Value); case SdAccountExpanded: // Type: Bool return Opts->SetValue(Identity.OptionName(OPT_AccountExpanded, k, sizeof(k)), Value); default: return false; } return true; } bool ScribeAccount::CallMethod(const char *MethodName, LVariant *ReturnValue, LArray &Args) { ScribeDomType m = StrToDom(MethodName); switch (m) { case SdStop: // Type: () Stop(); break; case SdDisconnect: // Type: () Disconnect(); break; case SdKill: // Type: () Kill(); break; case SdEndSession: // Type: () OnEndSession(); break; default: return false; } return true; } bool ScribeAccount::IsValid() { char Key[128]; Receive.OptionName(OPT_AccountUID, Key, sizeof(Key)); LVariant v; if (Parent->GetOptions()->GetValue(Key, v)) { return v.CastInt32() > 0; } return false; } bool ScribeAccount::Create() { LVariant v; bool Status = false; char Key[128]; Receive.OptionName(0, Key, sizeof(Key)); // Check our tag exists LXmlTag *Tag = Parent->GetOptions()->LockTag(Key, _FL); if (!Tag) { Parent->GetOptions()->CreateTag(Key); Tag = Parent->GetOptions()->LockTag(Key, _FL); } if (Tag) { // Check our ID exists if (!Tag->GetAttr(OPT_AccountUID)) { int NewId; do { NewId = LRand() & 0x7fffffff; } while (Parent->GetAccountById(NewId)); Tag->SetAttr(OPT_AccountUID, NewId); } // Create our child tags, if missing Tag->GetChildTag("Identity", true); Tag->GetChildTag("Send", true); Tag->GetChildTag("Receive", true); Status = true; Parent->GetOptions()->Unlock(); // Check for the defaults v = Receive.Name(); if (!v.Str()) { Receive.Name("My ISP"); } v = Send.PrefCharset1(); if (!v.Str()) { SetDefaults(); } } return Status; } bool ScribeAccount::Delete() { ReIndex(-1); while (Views.Length()) { LListItem *i = Views[0]; DeleteObj(i); } return true; } void ScribeAccount::SetDefaults() { // Set the default charsets... const char *DefCharset = LLoadString(IDS_DEFAULT_CHARSET_SEND); if (DefCharset) { - LToken t(DefCharset, ","); + auto t = LString(DefCharset).SplitDelimit(","); for (unsigned i=0; iIndex = i; } ScribeFolder *&ScribeAccount::GetRoot() { return Receive.Root; } LMenuItem *ScribeAccount::GetMenuItem() { return d->IdentityItem; } void ScribeAccount::SetMenuItem(LMenuItem *i) { d->IdentityItem = i; } void ScribeAccount::SetCheck(bool c) { if (d->IdentityItem) { d->IdentityItem->Checked(c); } } void ScribeAccount::CreateMaps() { char Name[256] = ""; Map(Send.OptionName(OPT_AccountName, Name, sizeof(Name)), IDC_ACCOUNT_NAME, GV_STRING); Identity.CreateMaps(); Send.CreateMaps(); Receive.CreateMaps(); } void ScribeAccount::ReIndex(int i) { char Key[128]; Receive.OptionName(0, Key, sizeof(Key)); EmptyMaps(); if (i >= 0) { LXmlTag *Tag = GetApp()->GetOptions()->LockTag(Key, _FL); if (Tag) { sprintf_s(Key, sizeof(Key), "Account-%i", i); Tag->SetTag(Key); d->Index = i; GetApp()->GetOptions()->Unlock(); } CreateMaps(); } else { GetApp()->GetOptions()->DeleteTag(Key); } } int ScribeAccount::GetIndex() { LAssert(d != NULL); return d ? d->Index : -1; } bool ScribeAccount::IsOnline() { return Send.IsOnline() || Receive.IsOnline(); } bool ScribeAccount::InitMenus() { bool Status = Send.InitMenus(); return Receive.InitMenus() && Status; } void ScribeAccount::Stop() { if (Send.IsOnline()) { Send.Disconnect(); } if (Receive.IsOnline()) { Receive.Disconnect(); } } void ScribeAccount::Kill() { if (Send.IsOnline()) { Send.Kill(); } if (Receive.IsOnline()) { Receive.Kill(); } } void ScribeAccount::OnPulse(char *s, int s_len) { Send.OnPulse(s, s_len); Receive.OnPulse(s, s_len); } void ScribeAccount::SerializeUi(LView *Wnd, bool Load) { if (Wnd) { LVariant v; // Special Fields LEdit *Pop3Folder; if (Wnd->GetViewById(IDC_FOLDER, Pop3Folder)) { Pop3Folder->Enabled(false); } GPassword s, r; char Buf[128]; if (Load) { LEdit *e; bool HasPass; HasPass = s.Serialize(Parent->GetOptions(), Receive.OptionName(OPT_EncryptedSmtpPassword, Buf, sizeof(Buf)), false); Wnd->SetCtrlValue(IDC_SMTP_AUTH, HasPass); if (HasPass && Wnd->GetViewById(IDC_SMTP_PASSWORD, e)) e->SetEmptyText(LLoadString(IDS_PASSWORD_SAVED)); HasPass = r.Serialize(Parent->GetOptions(), Receive.OptionName(OPT_EncryptedPop3Password, Buf, sizeof(Buf)), false); Wnd->SetCtrlValue(IDC_REMEMBER_PSW, HasPass); Wnd->SetCtrlEnabled(IDC_REC_PASSWORD, HasPass); if (HasPass && Wnd->GetViewById(IDC_REC_PASSWORD, e)) e->SetEmptyText(LLoadString(IDS_PASSWORD_SAVED)); } else { if (Wnd->GetCtrlValue(IDC_REMEMBER_PSW)) { const char *p = Wnd->GetCtrlName(IDC_REC_PASSWORD); if (ValidStr(p)) { r.Set(p); r.Serialize(Parent->GetOptions(), Receive.OptionName(OPT_EncryptedPop3Password, Buf, sizeof(Buf)), true); } } else r.Delete(Parent->GetOptions(), Receive.OptionName(OPT_EncryptedPop3Password, Buf, sizeof(Buf))); if (Wnd->GetCtrlValue(IDC_SMTP_AUTH)) { const char *p = Wnd->GetCtrlName(IDC_SMTP_PASSWORD); if (ValidStr(p)) { s.Set(p); s.Serialize(Parent->GetOptions(), Send.OptionName(OPT_EncryptedSmtpPassword, Buf, sizeof(Buf)), true); } } else s.Delete(Parent->GetOptions(), Send.OptionName(OPT_EncryptedSmtpPassword, Buf, sizeof(Buf))); } // Normal Fields Convert(Parent->GetOptions(), Wnd, Load); } } void ScribeAccount::InitUI(LView *Parent, int Tab, std::function callback) { auto Dlg = new AccountDlg(Parent, this->Parent, this, Tab); Dlg->DoModal([callback](auto dlg, auto id) { if (callback) callback(id); delete dlg; }); } int ScribeAccount::OnNotify(LViewI *Ctrl, LNotification &n) { LWindow *Wnd = Ctrl->GetWindow(); if (!Wnd) return 0; switch (Ctrl->GetId()) { case IDC_REMEMBER_PSW: { Wnd->SetCtrlEnabled(IDC_REC_PASSWORD, Ctrl->Value() != 0); if (Ctrl->Value()) { LViewI *c = Wnd->FindControl(IDC_REC_PASSWORD); if (c && c->IsAttached()) { c->Focus(true); } } else { Wnd->SetCtrlName(IDC_REC_PASSWORD, 0); } break; } case IDC_SMTP_AUTH: { Wnd->SetCtrlEnabled(IDC_SMTP_PASSWORD, Ctrl->Value() != 0); break; } case IDC_PICK_FOLDER: { auto Dlg = new FolderDlg(Parent, Parent); Dlg->DoModal([this, Dlg, Ctrl](auto dlg, auto id) { if (id) Ctrl->GetWindow()->SetCtrlName(IDC_FOLDER, Dlg->Get()); delete dlg; }); break; } } return 0; } bool ScribeAccount::Disconnect() { if (Send.IsOnline()) { Send.Disconnect(); } if (Receive.IsOnline()) { Receive.Disconnect(); } return true; } //////////////////////////////////////////////////////////////////////////// AccountThread::AccountThread(Accountlet *acc) : LThread("AccountThread") { DeleteOnExit = false; Acc = acc; } AccountThread::~AccountThread() { if (!LAppInst->InThread()) { LAssert(0); } if (!IsExited()) Cancel(true); uint64 Start = LCurrentTime(); while (!IsExited()) { // Give the thread a tiny bit of time to exit // It's just got to return after posting the message LSleep(100); if (LCurrentTime() - Start > 1000) { // Took too long... is it really exiting? LAssert(0); } if (LCurrentTime() - Start > 30000) { // Broken... cut out losses and exit. Terminate(); break; } } } void AccountThread::OnAfterMain() { if (Acc && Acc->GetApp()) { Acc->GetApp()->PostEvent(M_SCRIBE_THREAD_DONE, (LMessage::Param)this, (LMessage::Param)Acc); } else { LAssert(0); } } void AccountThread::Disconnect() { Cancel(true); } bool AccountThread::Kill() { Cancel(true); Terminate(); return true; } diff --git a/Code/ScribeApp.cpp b/Code/ScribeApp.cpp --- a/Code/ScribeApp.cpp +++ b/Code/ScribeApp.cpp @@ -1,12726 +1,12725 @@ /* ** FILE: ScribeApp.cpp ** AUTHOR: Matthew Allen ** DATE: 22/10/1998 ** DESCRIPTION: Scribe email application ** ** Copyright (C) 1998, Matthew Allen ** fret@memecode.com */ // Debug defines // #define PRINT_OUT_STORAGE_TREE // #define TEST_OBJECT_SIZE #define USE_SPELLCHECKER 1 #define USE_INTERNAL_BROWSER 1 // for help #define RUN_STARTUP_SCRIPTS 1 #define PROFILE_ON_PULSE 0 #define TRAY_CONTACT_BASE 1000 #define TRAY_MAIL_BASE 10000 // Includes #include #include #include #include #include #include "Scribe.h" #include "lgi/common/StoreConvert1To2.h" #include "lgi/common/NetTools.h" #include "lgi/common/ProgressDlg.h" #include "lgi/common/TextLabel.h" #include "lgi/common/Button.h" #include "lgi/common/CheckBox.h" #include "lgi/common/OpenSSLSocket.h" #include "lgi/common/SoftwareUpdate.h" #include "lgi/common/Html.h" #include "lgi/common/TextView3.h" #include "lgi/common/RichTextEdit.h" #include "lgi/common/Browser.h" #include "lgi/common/ClipBoard.h" #include "lgi/common/Store3.h" #include "lgi/common/Growl.h" #include "lgi/common/Edit.h" #include "lgi/common/Box.h" #include "lgi/common/LgiRes.h" #include "lgi/common/SpellCheck.h" #include "lgi/common/SubProcess.h" #include "lgi/common/CssTools.h" #include "lgi/common/Map.h" #include "lgi/common/Charset.h" #include "lgi/common/RefCount.h" #include "ScribePrivate.h" #include "PreviewPanel.h" #include "ScribeStatusPanel.h" #include "ScribeFolderDlg.h" #include "ScribePageSetup.h" #include "Calendar.h" #include "CalendarView.h" #include "ScribeSpellCheck.h" #include "Store3Common.h" #include "PrintContext.h" #include "resource.h" #include "ManageMailStores.h" #include "ReplicateDlg.h" #include "ScribeAccountPreview.h" #include "Encryption/GnuPG.h" #include "Store3Webdav/WebdavStore.h" #include "../Resources/resdefs.h" #include "../UnitTests/UnitTest.h" #include "../src/common/Coding/ScriptingPriv.h" #define DEBUG_STORE_EVENTS 0 #if DEBUG_STORE_EVENTS #define LOG_STORE(...) LgiTrace(__VA_ARGS__) #else #define LOG_STORE(...) #endif #define IDM_LOAD_MSG 2000 #define RAISED_LOOK 0 #define SUNKEN_LOOK false #ifdef MAC #define SUNKEN_CTRL false #else #define SUNKEN_CTRL true #endif #if LGI_CARBON #define TRAY_ICON_NONE -1 #define TRAY_ICON_NORMAL -1 #define TRAY_ICON_MAIL 0 #define TRAY_ICON_ERROR 1 #elif defined(WIN32) #define TRAY_ICON_NORMAL 0 #define TRAY_ICON_ERROR 1 #define TRAY_ICON_MAIL 2 #define TRAY_ICON_NONE 3 #else #define TRAY_ICON_NONE -1 #define TRAY_ICON_NORMAL 0 #define TRAY_ICON_ERROR 1 #define TRAY_ICON_MAIL 2 #endif char ScribeThingList[] = "com.memecode.ThingList"; ScribeClipboardFmt *ScribeClipboardFmt::Alloc(bool ForFolders, size_t Size) { ScribeClipboardFmt *obj = (ScribeClipboardFmt*) calloc(sizeof(ScribeClipboardFmt)+((Size-1)*sizeof(Thing*)), 1); if (obj) { memcpy(obj->Magic, ForFolders ? ScribeFolderMagic : ScribeThingMagic, sizeof(obj->Magic)); obj->ProcessId = LAppInst->GetProcessId(); obj->Len = (uint32_t)Size; } return obj; } ScribeClipboardFmt *ScribeClipboardFmt::Alloc(List &Lst) { ScribeClipboardFmt *Fmt = Alloc(false, Lst.Length()); for (unsigned i=0; iThingAt(i, Lst[i]); return Fmt; } ScribeClipboardFmt *ScribeClipboardFmt::Alloc(LArray &Arr) { ScribeClipboardFmt *Fmt = Alloc(false, Arr.Length()); for (unsigned i=0; iThingAt(i, Arr[i]); return Fmt; } bool ScribeClipboardFmt::Is(const char *Type, void *Ptr, size_t Bytes) { // Do we have the minimum bytes for the structure? if (Bytes >= sizeof(ScribeClipboardFmt) && Ptr != NULL) { ScribeClipboardFmt *This = (ScribeClipboardFmt*)Ptr; // Check the magic is the right value if (memcmp(This->Magic, Type, 4) != 0) return false; // Check it's from this process if (This->ProcessId != LAppInst->GetProcessId()) return false; return true; } return false; } Thing *ScribeClipboardFmt::ThingAt(size_t Idx, Thing *Set) { if (memcmp(Magic, ScribeThingMagic, 4)) return NULL; if (Idx >= Len) return NULL; if (Set) Things[Idx] = Set; return Things[Idx]; } ScribeFolder *ScribeClipboardFmt::FolderAt(size_t Idx, ScribeFolder *Set) { if (memcmp(Magic, ScribeFolderMagic, 4)) return NULL; if (Idx >= Len) return NULL; if (Set) Folders[Idx] = Set; return Folders[Idx]; } size_t ScribeClipboardFmt::Sizeof() { return sizeof(*this) + ((Len - 1) * sizeof(Thing*)); } bool OptionSizeInKiB = false; bool ShowRelativeDates = false; const char *MailAddressDelimiters = "\t\r\n;,"; char16 SpellDelim[] = { ' ', '\t', '\r', '\n', ',', ',', '.', ':', ';', '{', '}', '[', ']', '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '_', '-', '+', '=', '|', '\\', '/', '?', '\"', 0 }; const char *DefaultRfXml = "---------- %s ----------\n" "%s: ()\n" "%s: ()\n" "%s: \n" "%s: \n" "\n" "\n" "\n" "\n"; uchar DateTimeFormats[] = { GDTF_DEFAULT, GDTF_DAY_MONTH_YEAR | GDTF_12HOUR, GDTF_MONTH_DAY_YEAR | GDTF_12HOUR, GDTF_YEAR_MONTH_DAY | GDTF_12HOUR, GDTF_DAY_MONTH_YEAR | GDTF_24HOUR, GDTF_MONTH_DAY_YEAR | GDTF_24HOUR, GDTF_YEAR_MONTH_DAY | GDTF_24HOUR }; SystemFolderInfo SystemFolders[] = { {FOLDER_INBOX, OPT_Inbox, NULL}, {FOLDER_OUTBOX, OPT_Outbox, NULL}, {FOLDER_SENT, OPT_Sent, NULL}, {FOLDER_CONTACTS, OPT_Contacts, NULL}, {FOLDER_TRASH, OPT_Trash, NULL}, {FOLDER_CALENDAR, OPT_Calendar, OPT_HasCalendar}, {FOLDER_TEMPLATES, OPT_Templates, OPT_HasTemplates}, {FOLDER_FILTERS, OPT_Filters, OPT_HasFilters}, {FOLDER_GROUPS, OPT_Groups, OPT_HasGroups}, {FOLDER_SPAM, OPT_SpamFolder, OPT_HasSpam}, {-1, 0, 0} }; ScribeBehaviour *ScribeBehaviour::New(ScribeWnd *app) { return 0; } void ScribeOptionsDefaults(LOptionsFile *f) { if (!f) return; f->CreateTag("Accounts"); f->CreateTag("CalendarUI"); f->CreateTag("CalendarUI.Sources"); f->CreateTag("MailUI"); f->CreateTag("ScribeUI"); f->CreateTag("Plugins"); f->CreateTag("Print"); #define DefaultIntOption(opt, def) { LVariant v; if (!f->GetValue(opt, v)) \ f->SetValue(opt, v = (int)def); } #define DefaultStrOption(opt, def) { LVariant v; if (!f->GetValue(opt, v)) \ f->SetValue(opt, v = def); } DefaultIntOption(OPT_DefaultAlternative, 1); DefaultIntOption(OPT_BoldUnread, 1); DefaultIntOption(OPT_PreviewLines, 1); DefaultIntOption(OPT_AutoDeleteExe, 1); DefaultIntOption(OPT_DefaultReplyAllSetting, MAIL_ADDR_BCC); DefaultIntOption(OPT_BlinkNewMail, 1); DefaultIntOption(OPT_MarkReadAfterSeconds, 5); DefaultStrOption(OPT_BayesThreshold, "0.9"); DefaultIntOption(OPT_SoftwareUpdate, 1); DefaultIntOption(OPT_ResizeImgAttachments, false); DefaultIntOption(OPT_ResizeJpegQual, 80); DefaultIntOption(OPT_ResizeMaxPx, 1024); DefaultIntOption(OPT_ResizeMaxKb, 200); DefaultIntOption(OPT_RegisterWindowsClient, 1); DefaultIntOption(OPT_HasTemplates, 0); DefaultIntOption(OPT_HasCalendar, 1); DefaultIntOption(OPT_HasGroups, 1); DefaultIntOption(OPT_HasFilters, 1); DefaultIntOption(OPT_HasSpam, 0); } const char *Store3ItemTypeName(Store3ItemTypes t) { switch (t) { case MAGIC_NONE: return "MAGIC_NONE"; case MAGIC_BASE: return "MAGIC_BASE"; case MAGIC_MAIL: return "MAGIC_MAIL"; case MAGIC_CONTACT: return "MAGIC_CONTACT"; // case MAGIC_FOLDER: return "MAGIC_FOLDER"; case MAGIC_MAILBOX: return "MAGIC_MAILBOX"; case MAGIC_ATTACHMENT: return "MAGIC_ATTACHMENT"; case MAGIC_ANY: return "MAGIC_ANY"; case MAGIC_FILTER: return "MAGIC_FILTER"; case MAGIC_FOLDER: return "MAGIC_FOLDER"; case MAGIC_CONDITION: return "MAGIC_CONDITION"; case MAGIC_ACTION: return "MAGIC_ACTION"; case MAGIC_CALENDAR: return "MAGIC_CALENDAR"; case MAGIC_ATTENDEE: return "MAGIC_ATTENDEE"; case MAGIC_GROUP: return "MAGIC_GROUP"; default: LAssert(0); break; } return "(error)"; } void SetRecipients(ScribeWnd *App, char *Start, LDataIt l, EmailAddressType CC) { while (Start && *Start) { LString Str; char *End = strchr(Start, ','); if (End) { Str.Set(Start, End-Start); Start = End + 1; } else { Str = Start; Start = 0; } if (Str) { ListAddr *a = new ListAddr(App); if (a) { a->CC = CC; if (_strnicmp(Str, "mailto:", 7) == 0) a->sAddr = Str(7,-1); else a->sAddr = Str; l->Insert(a); } } } } static char SoftwareUpdateUri[] = "http://www.memecode.com/update.php"; enum SoftwareStatus { SwError, SwCancel, SwOutOfDate, SwUpToDate }; static LString ExtractVer(const char *s) { char Buf[256], *Out = Buf; for (const char *In = s; *In && Out < Buf + sizeof(Buf) - 1; In++) { if (*In == ' ') break; if (IsDigit(*In) || *In == '.') *Out++ = *In; } *Out++ = 0; return LString(Buf); } void IsSoftwareUpToDate(LSoftwareUpdate::UpdateInfo &Info, ScribeWnd *Parent, bool WithUI, bool IncBetas, std::function callback) { // Software update? LAutoString Proxy = Parent->GetHttpProxy(); LSoftwareUpdate Update(AppName, SoftwareUpdateUri, Proxy); Update.CheckForUpdate( Info, WithUI?Parent:0, IncBetas, [Info, WithUI, Parent, callback](auto status, auto errorMsg) { if (status) { auto LocalVer = LString(ScribeVer).SplitDelimit("."); LString BuildVer = ExtractVer(Info.Build); - LToken OnlineVer(BuildVer, "."); + auto OnlineVer = BuildVer.SplitDelimit("."); if (OnlineVer.Length() != LocalVer.Length()) { LgiTrace("%s:%i - Invalid online version number \"%s\"\n", _FL, Info.Version.Get()); if (callback) callback(SwError); return; } unsigned i; for (i=0; i o) { if (callback) callback(SwUpToDate); return; } } LDateTime Compile; - LToken Date(__DATE__, " "); + auto Date = LString(__DATE__).SplitDelimit(" "); Compile.Month(LDateTime::MonthFromName(Date[0])); Compile.Day(atoi(Date[1])); Compile.Year(atoi(Date[2])); Compile.SetTime(__TIME__); bool DateGreaterThenCompile = Info.Date > Compile; if (callback) callback(DateGreaterThenCompile ? SwOutOfDate : SwUpToDate); return; } else if (WithUI) { if (Info.Cancel) { if (callback) callback(SwCancel); return; } LgiMsg(Parent, LLoadString(IDS_ERROR_SOFTWARE_UPDATE), AppName, MB_OK, errorMsg); } if (callback) callback(SwError); }); } bool UpgradeSoftware(const LSoftwareUpdate::UpdateInfo &Info, ScribeWnd *Parent, bool WithUI) { bool DownloadUpdate = true; if (WithUI) { char Ds[64]; Info.Date.Get(Ds, sizeof(Ds)); DownloadUpdate = LgiMsg(Parent, LLoadString(IDS_SOFTWARE_UPDATE_DOWNLOAD), AppName, MB_YESNO, (char*)Info.Build, (char*)Info.Uri, Ds) == IDYES; } if (!DownloadUpdate) return false; LAutoString Proxy = Parent->GetHttpProxy(); LSoftwareUpdate Update(AppName, SoftwareUpdateUri, Proxy, ScribeTempPath()); return Update.ApplyUpdate(Info, false, Parent); } void SoftwareUpdate(ScribeWnd *Parent, bool WithUI, bool IncBetas, std::function callback) { // Software update? LSoftwareUpdate::UpdateInfo Info; IsSoftwareUpToDate(Info, Parent, WithUI, IncBetas, [WithUI, Parent, Info, callback](auto s) { if (s == SwUpToDate) { if (WithUI) LgiMsg(Parent, LLoadString(IDS_SOFTWARE_CURRENT), AppName, MB_OK); if (callback) callback(false); // we're up to date } else if (s == SwOutOfDate) { auto status = UpgradeSoftware(Info, Parent, WithUI); if (callback) callback(status); // update is going to happen } }); } const char *AppName = "Scribe"; char HelpFile[] = "index.html"; const char OptionsFileName[] = "ScribeOptions"; const char AuthorEmailAddr[] = "fret@memecode.com"; const char AuthorHomepage[] = "http://www.memecode.com"; const char ApplicationHomepage[] = "http://www.memecode.com/scribe.php"; const char CommercialHomepage[] = "http://www.memecode.com/inscribe.php"; const char FaqHomepage[] = "http://www.memecode.com/scribe/faq.php"; const char *DefaultFolderNames[16]; Store3ItemTypes DefaultFolderTypes[] = { MAGIC_MAIL, // Inbox MAGIC_MAIL, // Outbox MAGIC_MAIL, // Sent MAGIC_ANY, // Trash MAGIC_CONTACT, // Contacts MAGIC_MAIL, // Templates MAGIC_FILTER, // Filters MAGIC_CALENDAR, // Calendar Events MAGIC_GROUP, // Groups MAGIC_MAIL, // Spam MAGIC_NONE, MAGIC_NONE, MAGIC_NONE, MAGIC_NONE }; extern void Log(char *File, char *Str, ...); ////////////////////////////////////////////////////////////////////////////// void LogMsg(char *str, ...) { #ifdef _DEBUG char f[256]; LMakePath(f, sizeof(f), LGetExePath(), "log.txt"); if (str) { char buffer[256]; va_list arg; va_start(arg ,str); vsprintf_s(buffer, sizeof(buffer), str, arg); va_end(arg); LFile File; while (!File.Open(f, O_WRITE)) { LSleep(5); } File.Seek(File.GetSize(), SEEK_SET); File.Write(buffer, strlen(buffer)); } else { FileDev->Delete(f, false); } #endif } LString GetFullAppName(bool Platform) { LString Ret = AppName; if (Platform) { LString s; const char *Build = #ifndef _DEBUG "Release"; #else "Debug"; #endif LArray Ver; int Os = LGetOs(&Ver); const char *OsName = LGetOsName(); if (Os == LGI_OS_WIN9X) { switch (Ver[1]) { case 0: OsName = "Win95"; break; case 10: OsName = "Win98"; break; case 90: OsName = "WinME"; break; } } else if (Os == LGI_OS_WIN32 || Os == LGI_OS_WIN64) { if (Ver[0] < 5) { OsName = "WinNT"; } else if (Ver[0] == 5) { if (Ver[1] == 0) OsName = "Win2k"; else OsName = "WinXP"; } else if (Ver[0] == 6) { if (Ver[1] == 0) OsName = "Vista"; else if (Ver[1] == 1) OsName = "Win7"; else if (Ver[1] == 2) OsName = "Win8"; else if (Ver[1] == 3) OsName = "Win8.1"; } else if (Ver[0] == 10) { OsName = "Win10"; } else if (Ver[0] == 11) { // What's the chances eh? OsName = "Win11"; } } s.Printf(" v%s (%s v", ScribeVer, OsName); Ret += s; for (unsigned i=0; iId); Ret += s; } s.Printf(")"); Ret += s; } return Ret; } bool MatchWord(char *Str, char *Word) { bool Status = false; if (Str && Word) { #define IsWord(c) ( IsDigit(c) || IsAlpha(c) ) for (char *s=stristr(Str, Word); s; s=stristr(s+1, Word)) { char *e = s + strlen(Word); if ( (s<=Str || !IsWord(s[-1]) ) && (e[0] == 0 || !IsWord(e[0])) ) { return true; } } } return Status; } ////////////////////////////////////////////////////////////////////////////// ScribePanel::ScribePanel(ScribeWnd *app, const char *name, int size, bool open) : LPanel(name, size, open) { App = app; } bool ScribePanel::Pour(LRegion &r) { if (App) { SetClosedSize(App->GetToolbarHeight()); } return LPanel::Pour(r); } ////////////////////////////////////////////////////////////////////////////// class NoContactType : public Contact { LString NoFace80Path; LString NoFace160Path; public: NoContactType(ScribeWnd *wnd) : Contact(wnd) { } Thing &operator =(Thing &c) override { return *this; } bool GetVariant(const char *Name, LVariant &Value, const char *Array) override { ScribeDomType Fld = StrToDom(Name); int Px = Array ? atoi(Array) : 80; LString &Str = Px == 160 ? NoFace160Path : NoFace80Path; if (!Str) { LString f; f.Printf("NoFace%i.png", Px); Str = LFindFile(f); LAssert(Str != NULL); // This should always resolve. } if (!Str) return false; if (Fld == SdImageHtml) { LString html; html.Printf("\n", Str.Get()); Value = html; return true; } else if (Fld == SdImage) { Value = Str; return true; } return false; } }; class ScribeWndPrivate : public LBrowser::LBrowserEvents, public LVmDebuggerCallback, public LHtmlStaticInst { LOptionsFile::PortableType InstallMode = LOptionsFile::UnknownMode; public: ScribeWnd *App; uint64 LastTs = 0; int ClipboardFormat = 0; LFont *PreviewFont = NULL; int PrintMaxPages = -1; int NewMailTimeout = -1; bool SendAfterReceive = false; bool IngoreOnClose = false; LAutoString UiTags; LAutoPtr Growl; LArray TrayMenuContacts; bool ExitAfterSend = false; LToolButton *ShowConsoleBtn = NULL; LString MulPassword; LBox *SubSplit = NULL, *SearchSplit = NULL; LArray ThingSources; int LastLayout = 0; LMenuItem *DisableUserFilters = NULL; LOptionsFile *Options = NULL; HttpImageThread *ImageLoader = NULL; int LastMinute = -1, LastHour = -1; LArray Store3EventCallbacks; LAutoPtr PrintOptions; LHashTbl, LString> ResFiles; // These are for the LDataEventsI callbacks to store source context // Mainly for debugging where various events came from. const char *CtxFile = NULL; int CtxLine = 0; // Contact no face images LAutoRefPtr NoContact; // Remote content white/blacklists bool RemoteContent_Init = false; LString::Array RemoteWhiteLst, RemoteBlackLst; // Spell checking int AppWndHnd; LAutoPtr SpellerThread; // Missing caps LCapabilityTarget::CapsHash MissingCaps; MissingCapsBar *Bar = NULL; LString ErrSource; // Script file that has an error. Filter *ErrFilter = NULL; // Filter that has scripting error. // Load state bool FoldersLoaded = false; // Bayesian filter LStringPipe BayesLog; // Thread item processing LArray Transfers; // Scripting... LAutoPtr Engine; LArray Scripts; LArray CurrentScripts; LScript *CurrentScript() { return CurrentScripts.Length() ? CurrentScripts.Last() : NULL; } int NextToolMenuId = IDM_TOOL_SCRIPT_BASE; LAutoPtr ScriptToolbar; LArray OnSecondTimerCallbacks; // Encryption LAutoPtr GpgInst; // Unit tests LAutoPtr UnitTestServer; class ScribeTextControlFactory : public LViewFactory { ScribeWnd *Wnd; LView *NewView(const char *Class, LRect *Pos, const char *Text) { if (!_stricmp(Class, "ScribeTextView")) return Wnd->CreateTextControl(-1, 0, true); return NULL; } public: ScribeTextControlFactory(ScribeWnd *wnd) { Wnd = wnd; } } TextControlFactory; ScribeWndPrivate(ScribeWnd *app) : App(app), TextControlFactory(app) { NoContact = new NoContactType(app); NoContact->DecRef(); // 2->1 AppWndHnd = LEventSinkMap::Dispatch.AddSink(App); #ifdef WIN32 ClipboardFormat = RegisterClipboardFormat( #ifdef UNICODE L"Scribe.Item" #else "Scribe.Item" #endif ); #endif LScribeScript::Inst = new LScribeScript(App); if (Engine.Reset(new LScriptEngine(App, LScribeScript::Inst, this))) Engine->SetConsole(LScribeScript::Inst->GetLog()); } ~ScribeWndPrivate() { // Why do we need this? ~LView will take care of it? // LEventSinkMap::Dispatch.RemoveSink(App); DeleteObj(Options); Scripts.DeleteObjects(); DeleteObj(ImageLoader); Engine.Reset(); DeleteObj(LScribeScript::Inst); } LGrowl *GetGrowl() { if (!Growl && Growl.Reset(new LGrowl)) { LAutoPtr r(new LGrowl::LRegister); r->App = "Scribe"; r->IconUrl = "http://memecode.com/images/scribe/growl-app.png"; LGrowl::LNotifyType &NewMail = r->Types.New(); NewMail.Name = "new-mail"; NewMail.IconUrl = "http://memecode.com/images/scribe/growl-new-mail.png"; NewMail.Enabled = true; LGrowl::LNotifyType &Cal = r->Types.New(); Cal.Name = "calendar"; Cal.IconUrl = "http://memecode.com/images/scribe/growl-calendar.png"; Cal.Enabled = true; LGrowl::LNotifyType &Debug = r->Types.New(); Debug.Name = "debug"; Debug.IconUrl = "http://memecode.com/images/scribe/growl-bug.png"; Debug.Enabled = false; LGrowl::LNotifyType &Info = r->Types.New(); Info.IconUrl = "http://memecode.com/images/scribe/growl-note.png"; Info.Name = "info"; Info.Enabled = true; Growl->Register(r); } return Growl; } LVmDebugger *AttachVm(LVirtualMachine *Vm, LCompiledCode *Code, const char *Assembly) { LVariant v; if (Options) Options->GetValue(OPT_ScriptDebugger, v); if (v.CastInt32()) return new LVmDebuggerWnd(App, this, Vm, Code, NULL); return NULL; } bool CompileScript(LAutoPtr &Output, const char *FileName, const char *Source) { LCompiler c; return c.Compile(Output, Engine->GetSystemContext(), LScribeScript::Inst, FileName, Source, NULL); } bool OnSearch(LBrowser *br, const char *txt) { char Path[256]; if (!App->GetHelpFilesPath(Path, sizeof(Path))) return false; - LToken Terms(txt, ", "); + auto Terms = LString(txt).SplitDelimit(", "); LStringPipe p; p.Print("\n

Search Results

\n
    \n"); LDirectory Dir; for (int b = Dir.First(Path, "*.html"); b; b = Dir.Next()) { if (!Dir.IsDir()) { char Path[256]; Dir.Path(Path, sizeof(Path)); LFile f; if (f.Open(Path, O_READ)) { LXmlTree t(GXT_NO_DOM); LXmlTag r; if (t.Read(&r, &f)) { char *PrevName = 0; char PrevUri[256] = ""; for (auto c: r.Children) { if (c->IsTag("a")) { char *Name = c->GetAttr("name"); if (Name) { PrevName = Name; } } else if (c->GetContent()) { bool Hit = false; for (unsigned i=0; !Hit && iGetContent(), Terms[i]) != 0; } if (Hit) { LStringPipe Uri(256); char *Leaf = strrchr(Path, DIR_CHAR); Leaf = Leaf ? Leaf + 1 : Path; Uri.Print("file://%s", Path); if (PrevName) Uri.Print("#%s", PrevName); LAutoString UriStr(Uri.NewStr()); if (_stricmp(UriStr, PrevUri)) { p.Print("
  • %s", UriStr.Get(), Leaf); if (PrevName) p.Print("#%s", PrevName); p.Print("\n"); strcpy_s(PrevUri, sizeof(PrevUri), UriStr); } } } } } } } } p.Print("
\n\n\n"); LAutoString Html(p.NewStr()); br->SetHtml(Html); return true; } void AskUserForInstallMode(std::function callback) { auto Dlg = new LAlert(App, AppName, LLoadString(IDS_PORTABLE_Q), LLoadString(IDS_HELP), LLoadString(IDS_DESKTOP), LLoadString(IDS_PORTABLE)); Dlg->SetButtonCallback(1, [&](auto idx) { App->LaunchHelp("install.html"); }); Dlg->DoModal([callback](auto dlg, auto Btn) { if (Btn == 1) { // Help LAssert(!"Help btn should use callback."); } else if (Btn == 2) { // Desktop if (callback) callback(LOptionsFile::DesktopMode); } else if (Btn == 3) { // Portable if (callback) callback(LOptionsFile::PortableMode); } else { delete dlg; LAppInst->Exit(1); } delete dlg; }); } LOptionsFile::PortableType GetInstallMode() { if (InstallMode == LOptionsFile::UnknownMode) { if (LAppInst->GetOption("portable")) { InstallMode = LOptionsFile::PortableMode; LgiTrace("Selecting portable mode based on -portable switch.\n"); } else if (LAppInst->GetOption("desktop")) { InstallMode = LOptionsFile::DesktopMode; LgiTrace("Selecting portable mode based on -desktop switch.\n"); } } if (InstallMode == LOptionsFile::UnknownMode) { bool PortableIsPossible = true; char Inst[MAX_PATH_LEN] = ""; LGetSystemPath(LSP_APP_INSTALL, Inst, sizeof(Inst)); // Do write check char Wr[MAX_PATH_LEN]; LMakePath(Wr, sizeof(Wr), Inst, "_write_test.txt"); LFile f; if (f.Open(Wr, O_WRITE)) { // Clean up f.Close(); FileDev->Delete(Wr, false); } else { // No write perms PortableIsPossible = false; } if (PortableIsPossible && LAppInst->IsElevated()) { // Check if the install is in some read only location: // e.g. c:\Program Files char Pm[MAX_PATH_LEN]; if (LGetSystemPath(LSP_USER_APPS, Pm, sizeof(Pm))) { size_t n = strlen(Pm); PortableIsPossible = _strnicmp(Pm, Inst, n) != 0; // LgiMsg(App, "%i\n%s\n%s", AppName, MB_OK, PortableIsPossible, Pm, Inst); } else LgiTrace("%s:%i - Failed to get paths.", _FL); } if (PortableIsPossible) { // Basically "ask the user" here... return LOptionsFile::UnknownMode; } else { InstallMode = LOptionsFile::DesktopMode; LgiTrace("Selecting Desktop based on lack of write permissions to install folder.\n"); } } return InstallMode; } void SetInstallMode(LOptionsFile::PortableType t) { InstallMode = t; } void DeleteCallbacks(LArray &Callbacks) { for (unsigned i=0; iGetMenu()->FindItem(Callbacks[i].Param); if (it) { it->Remove(); DeleteObj(it); } } } } }; ////////////////////////////////////////////////////////////////////////////// void UpgradeRfOption(ScribeWnd *App, const char *New, const char *Old, const char *Default) { LVariant v; /* App->GetOptions()->GetValue(New, v); if (v.Str()) { ScribePath *Path = new ScribePath(App, Old); if (Path) { char *Xml = LReadTextFile(*Path); if (Xml) { App->GetOptions()->SetValue(New, v = Xml); DeleteArray(Xml); } App->GetOptions()->DeleteValue(Old); DeleteObj(Path); } } */ if (Default && !App->GetOptions()->GetValue(New, v)) { App->GetOptions()->SetValue(New, v = Default); } } //////////////////////////////////////////////////////////////////////////// ScribeWnd::AppState ScribeWnd::ScribeState = ScribeConstructing; /* * This constructor is a little convoluted, but the basic idea is this: * * - Do some basic init. * - Attempt to load the options (could make portable/desktop mode clear) * - If the portable/desktop mode is unclear ask the user. * - Call AppConstruct1. * - If the UI language is not known, ask the user. * - Call AppConstruct2. * * Each time a dialog is needed the rest of the code needs to be in a callable function. * * It's important to note that the ScribeWnd::OnCreate method needs to be called after * the system Handle() is created, and after any dialogs in the ScribeWnd::ScribeWnd * constructor have finished. */ ScribeWnd::ScribeWnd() : BayesianFilter(this), CapabilityInstaller("Scribe", ScribeVer, "http://memecode.com/components/lookup.php", ScribeTempPath()), TrayIcon(this) { if (_Lock) _Lock->SetName("ScribeWnd"); // init some variables LApp::ObjInstance()->AppWnd = this; LCharsetSystem::Inst()->DetectCharset = ::DetectCharset; d = new ScribeWndPrivate(this); ScribeIpc = new LSharedMemory("Scribe", SCRIBE_INSTANCE_MAX * sizeof(ScribeIpcInstance)); if (ScribeIpc && ScribeIpc->GetPtr()) { ScribeIpcInstance *InstLst = (ScribeIpcInstance*) ScribeIpc->GetPtr(); for (int i=0; iMagic = SCRIBE_INSTANCE_MAGIC; ThisInst->Pid = LProcessId(); // LgiTrace("Install Scribe pid=%i to pos=%i\n", LProcessId(), i); } } } else DeleteObj(ScribeIpc); #ifndef WIN32 printf("%s\n", GetFullAppName(true).Get()); #endif auto Type = d->GetInstallMode(); if (Type == LOptionsFile::UnknownMode) { // This may make the mode more clear... if (LoadOptions()) Type = d->GetInstallMode(); } if (Type == LOptionsFile::UnknownMode) { d->AskUserForInstallMode([this](auto selectedMode) { d->SetInstallMode(selectedMode); if (!d->Options) d->Options = new LOptionsFile(selectedMode, OptionsFileName); Construct1(); }); } else Construct1(); } void ScribeWnd::Construct1() { if (!d->Options && !LoadOptions()) { ScribeState = ScribeExiting; return; } ScribeOptionsDefaults(d->Options); LVariant GlyphSub; if (GetOptions()->GetValue(OPT_GlyphSub, GlyphSub)) { bool UseGlyphSub = GlyphSub.CastInt32() != 0; LSysFont->SubGlyphs(UseGlyphSub); LSysBold->SubGlyphs(UseGlyphSub); LFontSystem::Inst()->SetDefaultGlyphSub(UseGlyphSub); } else { GetOptions()->SetValue(OPT_GlyphSub, GlyphSub = LFontSystem::Inst()->GetDefaultGlyphSub()); } { // Limit the size of the 'Scribe.txt' log file char p[MAX_PATH_LEN]; if (LgiTraceGetFilePath(p, sizeof(p))) { int64 Sz = LFileSize(p); #define MiB * 1024 * 1024 if (Sz > (3 MiB)) FileDev->Delete(p); } } // Process pre-UI options LVariant SizeAdj; int SzAdj = SizeAdj.CastInt32(); if (GetOptions()->GetValue(OPT_UiFontSize, SizeAdj) && (SzAdj = SizeAdj.CastInt32()) >= 0 && SzAdj < 5) { SzAdj -= 2; if (SzAdj) { int Pt = LSysFont->PointSize(); LSysFont->PointSize(Pt + SzAdj); LSysFont->Create(); LSysBold->PointSize(Pt + SzAdj); LSysBold->Create(); LFont *m = LMenu::GetFont(); if (m) { m->PointSize(m->PointSize() + SzAdj); m->Create(); } } } else { GetOptions()->SetValue(OPT_UiFontSize, SizeAdj = 2); } // Resources and languages SetLanguage(); // If no language set... LVariant LangId; if (!GetOptions()->GetValue(OPT_UiLanguage, LangId)) { // Ask the user... auto Dlg = new LanguageDlg(this); if (!Dlg->Ok) { delete Dlg; LgiMsg(this, "Failed to create language selection dialog.", "Scribe Error"); ScribeState = ScribeExiting; LCloseApp(); } else { Dlg->DoModal([this, Dlg](auto dlg, auto id) { if (id) { // Set the language in the options file LVariant v; GetOptions()->SetValue(OPT_UiLanguage, v = Dlg->Lang.Get()); // Reload the resource file... to get the new lang. LResources *Cur = LgiGetResObj(false); DeleteObj(Cur); SetLanguage(); Construct2(); } else // User canceled { ScribeState = ScribeExiting; LCloseApp(); } delete dlg; }); } } else Construct2(); } void ScribeWnd::Construct2() { #if 1 auto CurRes = LgiGetResObj(false); LVariant Theme; if (CurRes && GetOptions()->GetValue(OPT_Theme, Theme)) { auto Paths = ScribeThemePaths(); auto NoTheme = LLoadString(IDS_DEFAULT); if (Theme.Str() && Stricmp(NoTheme, Theme.Str())) { for (auto p: Paths) { LFile::Path Inst(p); Inst += Theme.Str(); if (Inst.Exists()) { CurRes->SetThemeFolder(Inst); d->Static->OnSystemColourChange(); break; } } } } #endif LoadCalendarStringTable(); ZeroObj(DefaultFolderNames); DefaultFolderNames[FOLDER_INBOX] = LLoadString(IDS_FOLDER_INBOX, "Inbox"); DefaultFolderNames[FOLDER_OUTBOX] = LLoadString(IDS_FOLDER_OUTBOX, "Outbox"); DefaultFolderNames[FOLDER_SENT] = LLoadString(IDS_FOLDER_SENT, "Sent"); DefaultFolderNames[FOLDER_TRASH] = LLoadString(IDS_FOLDER_TRASH, "Trash"); DefaultFolderNames[FOLDER_CONTACTS] = LLoadString(IDS_FOLDER_CONTACTS, "Contacts"); DefaultFolderNames[FOLDER_TEMPLATES] = LLoadString(IDS_FOLDER_TEMPLATES, "Templates"); DefaultFolderNames[FOLDER_FILTERS] = LLoadString(IDS_FOLDER_FILTERS, "Filters"); DefaultFolderNames[FOLDER_CALENDAR] = LLoadString(IDS_FOLDER_CALENDAR, "Calendar"); DefaultFolderNames[FOLDER_GROUPS] = LLoadString(IDS_FOLDER_GROUPS, "Groups"); DefaultFolderNames[FOLDER_SPAM] = LLoadString(IDS_SPAM, "Spam"); LStringPipe RfXml; RfXml.Print(DefaultRfXml, LLoadString(IDS_ORIGINAL_MESSAGE), LLoadString(FIELD_TO), LLoadString(FIELD_FROM), LLoadString(FIELD_SUBJECT), LLoadString(IDS_DATE)); { LAutoString Xml(RfXml.NewStr()); UpgradeRfOption(this, OPT_TextReplyFormat, "ReplyXml", Xml); UpgradeRfOption(this, OPT_TextForwardFormat, "ForwardXml", Xml); } LFontType t; if (t.GetSystemFont("small")) { d->PreviewFont = t.Create(); if (d->PreviewFont) { #if defined WIN32 d->PreviewFont->PointSize(8); #endif } } MoveOnScreen(); // Load global graphics LoadImageResources(); // Load time threads // Window name Name(AppName); SetSnapToEdge(true); ClearTempPath(); #if WINNATIVE SetStyle(GetStyle() & ~WS_VISIBLE); SetExStyle(GetExStyle() & ~WS_EX_ACCEPTFILES); CreateClassW32(AppName, LoadIcon(LProcessInst(), MAKEINTRESOURCE(IDI_APP))); #endif #if defined LINUX SetIcon("About64px.png"); LFinishXWindowsStartup(this); #endif ScribeState = ScribeConstructed; OnCreate(); } void ScribeWnd::Construct3() { if (ScribeState == ScribeConstructing) { // Constructor is still running, probably showing some UI. // Don't complete setup at this point. return; } // Load the styles LResources::StyleElement(this); // Main menu Menu = new LMenu(AppName); if (Menu) { Menu->Attach(this); if (Menu->Load(this, "ID_MENU", GetUiTags())) { LAssert(ImageList != NULL); Menu->SetImageList(ImageList, false); auto IdentityItem = Menu->FindItem(IDM_NO_IDENTITIES); if (IdentityItem) { IdentityMenu = IdentityItem->GetParent(); } CmdSend.MenuItem = Menu->FindItem(IDM_SEND_MAIL); auto NewMailMenu = Menu->FindItem(IDM_NEW_EMAIL); if (NewMailMenu) { MailMenu = NewMailMenu->GetParent(); } LVariant v; WorkOffline = Menu->FindItem(IDM_WORK_OFFLINE); if (WorkOffline && GetOptions()->GetValue(OPT_WorkOffline, v)) { WorkOffline->Checked(v.CastInt32() != 0); } if ((d->DisableUserFilters = Menu->FindItem(IDM_FILTERS_DISABLE))) { if (GetOptions()->GetValue(OPT_DisableUserFilters, v)) { d->DisableUserFilters->Checked(v.CastInt32() != 0); } } #if RUN_STARTUP_SCRIPTS // Run scripts in './Scripts' folder char s[MAX_PATH_LEN]; LMakePath(s, sizeof(s), ScribeResourcePath(), "Scripts"); if (!LDirExists(s)) LMakePath(s, sizeof(s), LGetSystemPath(LSP_APP_INSTALL), #if defined(LINUX) || defined(WINDOWS) "..\\" #endif "Scripts"); if (!LDirExists(s)) LgiTrace("%s:%i - Error: the scripts folder '%s' doesn't exist.\n", _FL, s); else { bool ErrorDsp = false; LDirectory Dir; for (int b = Dir.First(s); b; b = Dir.Next()) { if (Dir.IsDir()) continue; char *Ext = LGetExtension(Dir.GetName()); if (!Ext || _stricmp(Ext, "script") != 0) continue; Dir.Path(s, sizeof(s)); LStringPipe Log; char *Source = LReadTextFile(s); if (Source) { LScript *Cur = new LScript; if (Cur) { char Msg[256]; d->CurrentScripts.Add(Cur); LScribeScript::Inst->GetLog()->Write(Msg, sprintf_s(Msg, sizeof(Msg), "Compiling '%s'...\n", Dir.GetName())); LCompiler c; if (c.Compile( Cur->Code, d->Engine->GetSystemContext(), LScribeScript::Inst, s, Source, NULL)) { LFunctionInfo *Main = Cur->Code->GetMethod("Main"); if (Main) { LVirtualMachine Vm(d); LScriptArguments Args(&Vm); Args.New() = new LVariant((LDom*)this); d->Scripts.Add(Cur); if (Vm.ExecuteFunction( Cur->Code, Main, Args, LScribeScript::Inst->GetLog()) && Args.GetReturn()->CastInt32()) { d->CurrentScripts.Delete(Cur, true); Cur = NULL; } else { LgiTrace("Error: Script's main failed (%s)\n", Cur->Code->GetFileName()); if (Cur->Callbacks.Length()) d->DeleteCallbacks(Cur->Callbacks); d->Scripts.Delete(Cur); } Args.DeleteObjects(); } } else if (!ErrorDsp) { ErrorDsp = true; OnScriptCompileError(Source, NULL); } if (Cur) { d->CurrentScripts.Delete(Cur, true); DeleteObj(Cur); } } DeleteArray(Source); } } } #endif #define EnableItem(id, en) { auto i = Menu->FindItem(id); if (i) i->Enabled(en); } #define SetMenuIcon(id, ico) { auto i = Menu->FindItem(id); if (i) i->Icon(ico); } EnableItem(IDM_IMPORT_OUTLOOK_ITEMS, true); // SetMenuIcon(IDM_OPEN_FOLDERS, ICON_OPEN_FOLDER); SetMenuIcon(IDM_OPTIONS, ICON_OPTIONS); SetMenuIcon(IDM_SECURITY, ICON_LOCK); SetMenuIcon(IDM_CUT, ICON_CUT); SetMenuIcon(IDM_COPY, ICON_COPY); SetMenuIcon(IDM_PASTE, ICON_PASTE); SetMenuIcon(IDM_LAYOUT1, ICON_LAYOUT1); SetMenuIcon(IDM_LAYOUT2, ICON_LAYOUT2); SetMenuIcon(IDM_LAYOUT3, ICON_LAYOUT3); SetMenuIcon(IDM_LAYOUT4, ICON_LAYOUT4); SetMenuIcon(IDM_NEW_EMAIL, ICON_UNSENT_MAIL); SetMenuIcon(IDM_SET_READ, ICON_READ_MAIL); SetMenuIcon(IDM_SET_UNREAD, ICON_UNREAD_MAIL); SetMenuIcon(IDM_NEW_CONTACT, ICON_CONTACT); SetMenuIcon(IDM_NEW_GROUP, ICON_CONTACT_GROUP); SetMenuIcon(IDM_REPLY, ICON_FLAGS_REPLY); SetMenuIcon(IDM_REPLY_ALL, ICON_FLAGS_REPLY); SetMenuIcon(IDM_FORWARD, ICON_FLAGS_FORWARD); SetMenuIcon(IDM_BOUNCE, ICON_FLAGS_BOUNCE); SetMenuIcon(IDM_NEW_FILTER, ICON_FILTER); SetMenuIcon(IDM_FILTER_CURRENT_FOLDER, ICON_FOLDER_FILTERS); SetMenuIcon(IDM_MEMECODE, ICON_LINK); SetMenuIcon(IDM_HOMEPAGE, ICON_LINK); SetMenuIcon(IDM_SCRIBE_FAQ, ICON_LINK); SetMenuIcon(IDM_INSCRIBE_LINK, ICON_LINK); SetMenuIcon(IDM_VERSION_HISTORY, ICON_LINK); SetMenuIcon(IDM_DEBUG_INFO, ICON_LINK); SetMenuIcon(IDM_TUTORIALS, ICON_LINK); SetMenuIcon(IDM_FEEDBACK, ICON_UNREAD_MAIL); SetMenuIcon(IDM_HELP, ICON_HELP); LMenuItem *mi; if ( GetOptions()->GetValue(OPT_EditControl, v) && (mi = Menu->FindItem(IDM_HTML_EDITOR)) ) mi->Checked(v.CastInt32() != 0); Menu->SetPrefAndAboutItems(IDM_OPTIONS, IDM_ABOUT); } } // Initialize user interface SetupUi(); // Get some of the base submenu pointers. These are needed // for SetupAccounts to work correctly, e.g. populate the // send/receive/preview submenus. Folders need to be loaded // before this for the templates folder BuildDynMenus(); // Load accounts SetupAccounts(); // Recursively load folder tree LoadFolders([&](auto status) { // Redo it for the templates... now that load folders has completed. BuildDynMenus(); if (ScribeState == ScribeExiting) return; // Process command line OnCommandLine(); // Update the templates sub-menu now that the folders are loaded BuildDynMenus(); // Check registry settings SetDefaultHandler(); // Run on load scripts... LArray OnLoadCallbacks; if (GetScriptCallbacks(LOnLoad, OnLoadCallbacks)) { for (auto r: OnLoadCallbacks) { LVirtualMachine Vm; LScriptArguments Args(&Vm); Args.New() = new LVariant(this); ExecuteScriptCallback(*r, Args); Args.DeleteObjects(); } } ScribeState = ScribeRunning; }); } void ScribeWnd::SetLanguage() { LVariant LangId; if (GetOptions()->GetValue(OPT_UiLanguage, LangId)) { // Set the language to load... LAppInst->SetConfig("Language", LangId.Str()); } LResources::SetLoadStyles(true); // Load the resources (with the current lang) if (!LgiGetResObj(true, "Scribe")) { LgiMsg(NULL, "The resource file 'Scribe.lr8' is missing.", AppName); ScribeState = ScribeExiting; LCloseApp(); } } ScribeWnd::~ScribeWnd() { LAppInst->AppWnd = 0; SearchView = NULL; ScribeState = ScribeExiting; LScribeScript::Inst->ShowScriptingWindow(false); // Other cleanup... ClearTempPath(); ShutdownIpc(); SetPulse(); // Save anything thats still dirty in the folders... // just in case we crash during the shutdown phase. ScribeFolder *Cur = GetCurrentFolder(); if (Cur) Cur->SerializeFieldWidths(); SaveDirtyObjects(5000); // Tell the UI not to reference anything in the folders if (PreviewPanel) { PreviewPanel->OnThing(0, false); } Mail::NewMailLst.Empty(); // ~AccountStatusItem references the account list... must be before we // delete the accounts. DeleteObj(StatusPanel); // ~Accountlet needs to reference the root container... so // it has to go before unloading of folders. Accounts.DeleteObjects(); UnLoadFolders(); DeleteObj(PreviewPanel); SaveOptions(); DeleteObj(Commands); DeleteObj(d->PreviewFont); DeleteObj(d->SubSplit); DeleteObj(Splitter); MailList = NULL; CmdSend.ToolButton = NULL; CmdReceive.ToolButton = NULL; CmdPreview.ToolButton = NULL; CmdSend.MenuItem = NULL; CmdReceive.MenuItem = NULL; CmdPreview.MenuItem = NULL; // This could be using the OpenSSL library for HTTPS connections. So // close it before calling EndSSL. DeleteObj(d->ImageLoader); // This has to be after we close all the accounts... otherwise // they might still be using SSL functions, e.g. an IMAP/SSL connect. EndSSL(); DeleteObj(d); } LString ScribeWnd::GetResourceFile(SribeResourceType Type) { auto File = d->ResFiles.Find(Type); if (!File) LgiTrace("%s:%i - No file for resource type %i\n", _FL, Type); return File; } void ScribeWnd::LoadImageResources() { auto Res = LgiGetResObj(); LString::Array Folders; if (Res) { auto p = Res->GetThemeFolder(); if (p) Folders.Add(p); } Folders.Add(ScribeResourcePath()); for (auto p: Folders) { LDirectory Dir; LgiTrace("%s:%i - Loading resource folder '%s'\n", _FL, p.Get()); for (auto b = Dir.First(p); b; b = Dir.Next()) { if (Dir.IsDir()) continue; auto Name = Dir.GetName(); if (MatchStr("Toolbar-*.png", Name)) { if (!d->ResFiles.Find(ResToolbarFile)) d->ResFiles.Add(ResToolbarFile, Dir.FullPath()); } else if (MatchStr("xgate-icons-*.png", Name)) d->ResFiles.Add(ResToolbarFile, Dir.FullPath()); else if (MatchStr("Icons-*.png", Name)) d->ResFiles.Add(ResIconsFile, Dir.FullPath()); } } ToolbarImgs.Reset(LLoadImageList(GetResourceFile(ResToolbarFile))); ImageList.Reset(LLoadImageList(GetResourceFile(ResIconsFile))); if (!ImageList) LgiTrace("%s:%i - Failed to load toolbar image ('xgate-icons-32.png' or 'Toolbar-24.png')\n", _FL); } int ScribeWnd::GetEventHandle() { return d->AppWndHnd; } void ScribeWnd::OnCloseInstaller() { d->Bar = NULL; if (InThread()) { PourAll(); } else LAssert(0); } void ScribeWnd::OnInstall(CapsHash *Caps, bool Status) { } bool ScribeWnd::NeedsCapability(const char *Name, const char *Param) { #if DEBUG_CAPABILITIES LgiTrace("ScribeWnd::NeedsCapability(%s, %s)\n", Name, Param); #endif if (!InThread()) { #if DEBUG_CAPABILITIES LgiTrace("%s:%i - Posting M_NEEDS_CAP\n", _FL); #endif PostEvent(M_NEEDS_CAP, (LMessage::Param)NewStr(Name), (LMessage::Param)NewStr(Param)); } else { if (!Name) return false; if (d->MissingCaps.Find(Name)) { #if DEBUG_CAPABILITIES LgiTrace("%s:%i - Already in MissingCaps\n", _FL); #endif return true; } d->MissingCaps.Add(Name, true); LStringPipe MsgBuf(256); int i = 0; // const char *k; // for (bool b=d->MissingCaps.First(&k); b; b=d->MissingCaps.Next(&k), i++) for (auto k : d->MissingCaps) { MsgBuf.Print("%s%s", i?", ":"", k.key); } LVariant Actions; if (stristr(Name, "OpenSSL")) { MsgBuf.Print(LLoadString(IDS_ERROR_SERVER_CONNECT)); if (Param) MsgBuf.Print("\n%s", Param); Actions.Add(new LVariant(LLoadString(IDS_INSTALL))); } else if (stristr(Name, "Registry")) { MsgBuf.Print(LLoadString(IDS_ERROR_REG_WRITE)); Actions.Add(new LVariant(LLoadString(IDS_DONT_SHOW_AGAIN))); } else if (stristr(Name, "SpellingDictionary")) { MsgBuf.Print(LLoadString(IDS_ERROR_NEED_INSTALL), Param); Actions.Add(new LVariant(LLoadString(IDS_DOWNLOAD))); } Actions.Add(new LVariant(LLoadString(IDS_OK))); #if DEBUG_CAPABILITIES LgiTrace("%s:%i - Actions.Length()=%i, Bar=%p\n", _FL, Actions.Length(), d->Bar); #endif if (Actions.Length()) { LAutoString Msg(MsgBuf.NewStr()); // Check the script hook here... bool ShowInstallBar = true; LArray Callbacks; if (GetScriptCallbacks(LBeforeInstallBar, Callbacks)) { for (unsigned i=0; iCastInt32()) ShowInstallBar = false; else Msg.Reset(TheMsg.ReleaseStr()); } } } // Now create the capability install bar... if (!d->Bar && ShowInstallBar && Actions.Type == GV_LIST) { // FYI Capabilities are handled in ScribeWnd::StartAction. LArray Act; for (auto v : *Actions.Value.Lst) Act.Add(v->Str()); d->Bar = new MissingCapsBar(this, &d->MissingCaps, Msg, this, Act); AddView(d->Bar, 2); AttachChildren(); OnPosChange(); } } } return true; } LAutoString ScribeWnd::GetDataFolder() { LVariant v; GetOptions()->GetValue(OPT_IsPortableInstall, v); char p[MAX_PATH_LEN]; if (LGetSystemPath(v.CastInt32() ? LSP_APP_INSTALL : LSP_APP_ROOT, p, sizeof(p))) { if (!LDirExists(p)) FileDev->CreateFolder(p); return LAutoString(NewStr(p)); } else LgiTrace("%s:%i - LgiGetSystemPath failed (portable=%i).\n", _FL, v.CastInt32()); return LAutoString(); } LScriptEngine *ScribeWnd::GetScriptEngine() { return d->Engine; } LScriptCallback ScribeWnd::GetCallback(const char *CallbackMethodName) { LScriptCallback Cb; auto Cur = d->CurrentScript(); if (Cur && Cur->Code) { Cb.Script = Cur; Cb.Func = Cur->Code->GetMethod(CallbackMethodName); } if (!Cb.Func) { for (auto s: d->Scripts) { Cb.Script = s; if ((Cb.Func = s->Code->GetMethod(CallbackMethodName))) break; } } return Cb; } bool ScribeWnd::RegisterCallback(LScriptCallbackType Type, LScriptArguments &Args) { if (!d->CurrentScript()) { LgiTrace("%s:%i - No current script.\n", _FL); return false; } char *Fn = Args[1]->Str(); LScriptCallback Cb = GetCallback(Fn); if (!Cb.Func) { LgiTrace("%s:%i - No callback '%s'.\n", _FL, Fn); return false; } switch (Type) { case LToolsMenu: { char *Menu = Args[0]->Str(); auto Cur = d->CurrentScript(); if (!Menu || !Fn || !Cur) { LgiTrace("%s:%i - menu=%s, fn=%s.\n", _FL, Menu, Fn); return false; } LScriptCallback &c = Cur->Callbacks.New(); c = Cb; c.Type = Type; c.Param = d->NextToolMenuId; LMenuItem *Tools = GetMenu()->FindItem(IDM_TOOLS_MENU); auto ToolSub = Tools ? Tools->Sub() : 0; if (ToolSub) { if (d->NextToolMenuId == IDM_TOOL_SCRIPT_BASE) { ToolSub->AppendSeparator(); } ToolSub->AppendItem(Menu, c.Param, true); d->NextToolMenuId++; } break; } case LThingContextMenu: case LFolderContextMenu: case LThingUiToolbar: case LMailOnBeforeSend: case LMailOnAfterReceive: case LApplicationToolbar: case LBeforeInstallBar: case LInstallComponent: case LOnTimer: case LRenderMail: case LOnLoad: { auto Cur = d->CurrentScript(); LAssert(d->Scripts.HasItem(Cur)); LScriptCallback &c = Cur->Callbacks.New(); c = Cb; c.Type = Type; if (Args.Length() > 2) c.Data = *Args[2]; break; } default: { LAssert(!"Not a known callback type"); return false; } } return true; } bool ScribeWnd::GetScriptCallbacks(LScriptCallbackType Type, LArray &Callbacks) { for (auto s: d->Scripts) { for (auto &c: s->Callbacks) { if (c.Type == Type) Callbacks.Add(&c); } } return Callbacks.Length() > 0; } bool ScribeWnd::ExecuteScriptCallback(LScriptCallback &c, LScriptArguments &Args, bool ReturnArgs) { if (!c.Func || !c.Script) return false; // Setup LVirtualMachine Vm(d); d->CurrentScripts.Add(c.Script); // Call the method bool Status = Vm.ExecuteFunction( c.Script->Code, c.Func, Args, LScribeScript::Inst->GetLog(), ReturnArgs ? &Args : NULL) != ScriptError; // Cleanup d->CurrentScripts.PopLast(); return Status; } LStream *ScribeWnd::ShowScriptingConsole() { auto Item = Menu->FindItem(IDM_SCRIPTING_CONSOLE); if (Item) { Item->Checked(!Item->Checked()); LScribeScript::Inst->ShowScriptingWindow(Item->Checked()); LVariant v; GetOptions()->SetValue(OPT_ShowScriptConsole, v = Item->Checked()); } return LScribeScript::Inst->GetLog(); } LOptionsFile::PortableType ScribeWnd::GetPortableType() { return d->GetInstallMode(); } void ScribeWnd::RemoteContent_AddSender(const char *Addr, bool WhiteList) { if (!Addr) return; auto Opt = WhiteList ? OPT_RemoteContentWhiteList : OPT_RemoteContentBlackList; LVariant v; GetOptions()->GetValue(Opt, v); // Not an error if not there... auto existing = LString(v.Str()).SplitDelimit(" ,\r\n"); for (auto p: existing) { if (MatchStr(p, Addr)) { LgiTrace("%s:%i - '%s' is already in '%s'\n", _FL, Addr, Opt); return; // Already in list... } } existing.SetFixedLength(false); existing.Add(Addr); auto updated = LString("\n").Join(existing); GetOptions()->SetValue(Opt, v = updated.Get()); LgiTrace("%s:%i - Added '%s' to '%s'\n", _FL, Addr, Opt); d->RemoteContent_Init = false; } ScribeRemoteContent ScribeWnd::RemoteContent_GetSenderStatus(const char *Addr) { if (!d->RemoteContent_Init) { LVariant v; if (GetOptions()->GetValue(OPT_RemoteContentWhiteList, v)) d->RemoteWhiteLst = LString(v.Str()).SplitDelimit(" ,\r\n"); if (GetOptions()->GetValue(OPT_RemoteContentBlackList, v)) d->RemoteBlackLst = LString(v.Str()).SplitDelimit(" ,\r\n"); d->RemoteContent_Init = true; } for (auto p: d->RemoteWhiteLst) if (MatchStr(p, Addr)) return RemoteAlwaysLoad; for (auto p: d->RemoteBlackLst) if (MatchStr(p, Addr)) return RemoteNeverLoad; return RemoteDefault; } void ScribeWnd::RemoteContent_ClearCache() { d->RemoteWhiteLst.Empty(); d->RemoteBlackLst.Empty(); d->RemoteContent_Init = false; } void ScribeWnd::OnSpellerSettingChange() { // Kill the current thread d->SpellerThread.Reset(); // Setup the new thread LSpellCheck *t = GetSpellThread(); if (t) { // Trigger an install if needed t->Check(d->AppWndHnd, "thisisamispeltword", 0, 18); } } bool ScribeWnd::SetSpellThreadParams(LSpellCheck *Thread) { if (!Thread) return false; LVariant Lang, Dict; GetOptions()->GetValue(OPT_SpellCheckLanguage, Lang); GetOptions()->GetValue(OPT_SpellCheckDictionary, Dict); LAutoPtr Params(new LSpellCheck::Params); if (!Params) return false; Params->IsPortable = GetPortableType(); Params->OptionsPath = GetOptions()->GetFile(); Params->Lang = Lang.Str(); Params->Dict = Dict.Str(); Params->CapTarget = this; Thread->SetParams(Params); return true; } LSpellCheck *ScribeWnd::CreateSpellObject() { LVariant PrefAspell; GetOptions()->GetValue(OPT_PreferAspell, PrefAspell); LAutoPtr Obj; if (PrefAspell.CastInt32()) Obj = CreateAspellObject(); #if defined(MAC) if (!Obj) Obj = CreateAppleSpellCheck(); #elif defined(WINDOWS) LArray Ver; int Os = LGetOs(&Ver); if ( !Obj && (Os == LGI_OS_WIN32 || Os == LGI_OS_WIN64) && ( Ver.Length() > 1 && ( Ver[0] > 6 || (Ver[0] == 6 && Ver[1] > 1) ) ) ) Obj = CreateWindowsSpellCheck(); #endif if (!Obj) Obj = CreateAspellObject(); SetSpellThreadParams(Obj); return Obj.Release(); } LSpellCheck *ScribeWnd::GetSpellThread(bool OverrideOpt) { LVariant Use; if (OverrideOpt) Use = true; else GetOptions()->GetValue(OPT_SpellCheck, Use); #if USE_SPELLCHECKER if ((Use.CastInt32() != 0) ^ (d->SpellerThread.Get() != 0)) d->SpellerThread.Reset(Use.CastInt32() ? CreateSpellObject() : NULL); #endif return d->SpellerThread; } LAutoString ScribeWnd::GetHttpProxy() { LAutoString Proxy; LVariant v; if (GetOptions()->GetValue(OPT_HttpProxy, v) && ValidStr(v.Str())) { Proxy.Reset(v.ReleaseStr()); } else { LProxyUri p; if (p.sHost) Proxy.Reset(NewStr(p.ToString())); } return Proxy; } InstallProgress *ScribeWnd::StartAction(MissingCapsBar *Bar, LCapabilityTarget::CapsHash *Components, const char *ActionParam) { if (!ActionParam) { LgiTrace("%s:%i - No action supplied.\n", _FL); return NULL; } LArray Callbacks; LVariant Action(ActionParam); if (GetScriptCallbacks(LInstallComponent, Callbacks)) { bool StartInstall = true; for (unsigned i=0; iCastInt32()) StartInstall = false; } } if (!Action.Str()) { LgiTrace("%s:%i - GInstallComponent removed action name.\n", _FL); return NULL; } if (!StartInstall) { LgiTrace("%s:%i - GInstallComponent script canceled install of '%s'.\n", _FL, ActionParam); return NULL; } } if (!_stricmp(Action.Str(), LLoadString(IDS_OK))) { // Do nothing d->MissingCaps.Empty(); } else if (!_stricmp(Action.Str(), LLoadString(IDS_DONT_SHOW_AGAIN))) { // Turn off registering as a client. LVariant No(false); GetOptions()->SetValue(OPT_RegisterWindowsClient, No); GetOptions()->SetValue(OPT_CheckDefaultEmail, No); } else if (!_stricmp(Action.Str(), LLoadString(IDS_INSTALL))) { #ifdef WINDOWS bool IsSsl = false; for (auto c: *Components) { if (!_stricmp(c.key, "openssl")) { IsSsl = true; break; } } if (IsSsl) { LString s; s.Printf(LLoadString(IDS_WINDOWS_SSL_INSTALL), LGetOsName()); auto q = new LAlert(this, AppName, s, "Open Website", LLoadString(IDS_CANCEL)); q->DoModal([this, q](auto dlg, auto id) { switch (id) { case 1: LExecute("https://slproweb.com/products/Win32OpenSSL.html"); break; default: break; } delete dlg; }); return NULL; } #endif return CapabilityInstaller::StartAction(Bar, Components, Action.Str()); } else if (!_stricmp(Action.Str(), LLoadString(IDS_SHOW_CONSOLE))) { ShowScriptingConsole(); } else if (!_stricmp(Action.Str(), LLoadString(IDS_OPEN_SOURCE))) { if (d->ErrSource) LExecute(d->ErrSource); else if (d->ErrFilter) d->ErrFilter->DoUI(); d->ErrSource.Empty(); d->ErrFilter = NULL; } else if ( !Stricmp(Action.Str(), LLoadString(IDS_SHOW_REMOTE_CONTENT)) || !Stricmp(Action.Str(), LLoadString(IDS_ALWAYS_SHOW_REMOTE_CONTENT))) { auto c = Components->begin(); LWindow *w = Bar->GetWindow(); if ((*c).key && !Stricmp((*c).key, "RemoteContent") && w) { LVariant Ret, Always(!Stricmp(Action.Str(), LLoadString(IDS_ALWAYS_SHOW_REMOTE_CONTENT))); LArray Args; Args[0] = &Always; w->CallMethod(DomToStr(SdShowRemoteContent), &Ret, Args); } } else if (!_stricmp(Action.Str(), LLoadString(IDS_DOWNLOAD))) { auto t = GetSpellThread(); if (t) t->InstallDictionary(); else LgiTrace("%s:%i - No spell thread.\n", _FL); } else LAssert(!"Unknown action."); return NULL; } HttpImageThread *ScribeWnd::GetImageLoader() { if (!d->ImageLoader) { LAutoString Proxy = GetHttpProxy(); d->ImageLoader = new HttpImageThread(this, Proxy, 0); } return d->ImageLoader; } char *ScribeWnd::GetUiTags() { if (!d->UiTags) { char UiTags[256] = "inscribe" #if defined WIN32 " win32" #elif defined LINUX " linux" #elif defined MAC " mac" #endif ; LVariant Tags; if (!GetOptions()) { LAssert(!"Where is the options?"); } else if (GetOptions()->GetValue("tags", Tags)) { size_t Len = strlen(UiTags); sprintf_s(UiTags+Len, sizeof(UiTags)-Len, " %s", Tags.Str()); } d->UiTags.Reset(NewStr(UiTags)); } return d->UiTags; } void ScribeWnd::OnCreate() { LgiTrace("ScribeWnd::OnCreate. ScribeState=%i\n", ScribeState); if (IsAttached() && ScribeState == ScribeConstructed) { ScribeState = ScribeInitializing; Construct3(); } } ScribeAccount *ScribeWnd::GetAccountByEmail(const char *Email) { if (!Email) return NULL; for (auto a : *GetAccounts()) { LVariant e = a->Identity.Email(); if (e.Str() && !_stricmp(e.Str(), Email)) { return a; } } return 0; } ScribeAccount *ScribeWnd::GetAccountById(int Id) { for (auto a : *GetAccounts()) { if (a->Receive.Id() == Id) { return a; } } return 0; } const char *ScribeWnd::EditCtrlMimeType() { LVariant Html; GetOptions()->GetValue(OPT_EditControl, Html); return Html.CastInt32() ? sTextHtml : sTextPlain; } LAutoString ScribeWnd::GetReplyXml(const char *MimeType) { bool IsHtml = MimeType && !_stricmp(MimeType, sTextHtml); LVariant s; GetOptions()->GetValue(IsHtml ? OPT_HtmlReplyFormat : OPT_TextReplyFormat, s); return LAutoString(s.ReleaseStr()); } LAutoString ScribeWnd::GetForwardXml(const char *MimeType) { bool IsHtml = MimeType && !_stricmp(MimeType, sTextHtml); LVariant s; GetOptions()->GetValue(IsHtml ? OPT_HtmlForwardFormat : OPT_TextForwardFormat, s); return LAutoString(s.ReleaseStr()); } LVmDebuggerCallback *ScribeWnd::GetDebuggerCallback() { return d; } GpgConnector *ScribeWnd::GetGpgConnector() { if (!d->GpgInst) { if (!GpgConnector::IsInstalled()) return NULL; d->GpgInst.Reset(new GpgConnector()); } return d->GpgInst; } LFont *ScribeWnd::GetPreviewFont() { return d->PreviewFont; } bool ScribeWnd::IsValid() { #if 0 try { for (ScribeAccount *a = Accounts.First(); a; a = Accounts.Next()) { } } catch(...) { return false; } #endif return true; } bool ScribeWnd::ShutdownIpc() { // Remove our instance from the shared memory if (ScribeIpc && ScribeIpc->GetPtr()) { ScribeIpcInstance *InstLst = (ScribeIpcInstance*) ScribeIpc->GetPtr(); if (ThisInst) memset(ThisInst, 0, sizeof(*ThisInst)); int c = 0; for (int i=0; iDestroy(); } ThisInst = 0; DeleteObj(ScribeIpc); return true; } bool ScribeWnd::GetVariant(const char *Name, LVariant &Value, const char *Array) { ScribeDomType Fld = StrToDom(Name); switch (Fld) { case SdQuote: // Type: String { return GetOptions()->GetValue(OPT_QuoteReplyStr, Value); } case SdName: // Type: String { Value = AppName; break; } case SdHome: // Type: String { Value = LGetExePath().Get(); break; } case SdNow: // Type: String { LDateTime Now; Now.SetNow(); char s[64]; Now.Get(s, sizeof(s)); Value = s; break; } case SdFolder: // Type: ScribeFolder[] // Pass system folder index or string as array parameter. { ScribeFolder *f = 0; if (!Array) return false; if (IsDigit(*Array)) { f = GetFolder(atoi(Array)); } else { f = GetFolder(Array); } if (!f) return false; Value = (LDom*)f; break; } case SdAppName: // Type: String { Value = AppName; break; } case SdCalendarToday: // Type: String { return Calendar::SummaryOfToday(this, Value); } case SdInboxSummary: { LStringPipe p; // Iterate through the mail stores for (auto m: Folders) { if (!m.Store) continue; auto Inbox = m.Store->GetObj(FIELD_INBOX); if (!Inbox) continue; auto Unread = Inbox->GetInt(FIELD_UNREAD); auto Name = Inbox->GetStr(FIELD_FOLDER_NAME); LString Path; Path.Printf("/%s/%s", m.Name.Get(), Name); if (Unread) p.Print("%s: %i unread
", m.Name.Get(), Path.Get(), Unread); else p.Print("%s: 0 unread
", m.Name.Get()); } // And also the IMAP full folders for (auto a: Accounts) { ScribeProtocol Protocol = a->Receive.ProtocolType(); if (Protocol == ProtocolImapFull) { LDataStoreI *Store = a->Receive.GetDataStore(); if (Store) { auto Inbox = Store->GetObj(FIELD_INBOX); if (Inbox) { auto Unread = Inbox->GetInt(FIELD_UNREAD); auto Name = Inbox->GetStr(FIELD_FOLDER_NAME); auto m = a->Receive.Name(); if (m.Str()) { LString Path; Path.Printf("/%s/%s", m.Str(), Name); if (Unread) p.Print("%s: %i unread
", m.Str(), Path.Get(), Unread); else p.Print("%s: 0 unread
", m.Str()); } } } } } Value = p.NewLStr().Get(); break; } case SdExecute: // Type: String { if (!Array) return false; const char *s = Array; char *Exe = LTokStr(s); if (!Exe) return false; while (*s && *s == ' ') s++; LStringPipe Out; LSubProcess p(Exe, (char*)s); if (p.Start()) { p.Communicate(&Out); LAutoString o(Out.NewStr()); LAutoString t(TrimStr(o)); Value = t; } DeleteArray(Exe); break; } case SdBuildType: // Type: String { #ifdef _DEBUG Value = "Debug"; #else Value = "Release"; #endif break; } case SdPlatform: // Type: String { LArray Ver; LGetOs(&Ver); LString::Array Va; for (auto i: Ver) Va.New().Printf("%i", i); #if defined __GTK_H__ auto Api = "GTK3"; #elif LGI_SDL auto Api = "SDL"; #elif LGI_COCOA auto Api = "Cocoa"; #elif LGI_CARBON auto Api = "Carbon"; #elif defined WIN32 auto Api = "WinApi"; #else #error "Impl me." auto Api = "#err"; #endif LString s; s.Printf("%s, v%s, %s", LGetOsName(), LString(".").Join(Va).Get(), Api); Value = s.Get(); break; } case SdVersion: // Type: String { char Ver[32]; sprintf_s(Ver, sizeof(Ver), "v%s", ScribeVer); Value = Ver; break; } case SdBuild: // Type: String { char s[128]; sprintf_s(s, sizeof(s), "%s, %s, %ibit", __DATE__, __TIME__, (int)(sizeof(NativeInt)*8)); Value = s; break; } case SdLanguage: // Type: String { LLanguage *l = LGetLanguageId(); if (!l) return false; char s[256]; sprintf_s(s, sizeof(s), "%s \"%s\"", l->Name, l->Id); Value = s; break; } case SdString: // Type: String { if (!Array) { LAssert(!"Missing string ID"); return false; } int Id = atoi(Array); Value = LLoadString(Id); break; } case SdCurrentFolder: // Type: ScribeFolder { Value = (LDom*) GetCurrentFolder(); break; } case SdView: // Type: LView { Value = (LView*)this; break; } case SdNoContact: // Type: Contact { Value = (NoContactType*)d->NoContact; break; } case SdAccounts: { if (Array) { if (IsDigit(*Array)) { auto i = atoi(Array); if (i >= 0 && i < (ssize_t)Accounts.Length()) { Value = (LDom*)Accounts[i]; } else return false; } else { for (auto a : Accounts) { LVariant nm = a->Send.Name(); if (nm.Str() && !_stricmp(nm.Str(), Array)) { Value = (LDom*)a; break; } } } } else { Value = (int32)Accounts.Length(); } break; } case SdOptions: { Value = GetOptions(); break; } case SdMailStorePaths: { if (!Value.SetList()) return false; for (auto Ms : Folders) Value.Add(new LVariant(Ms.Path)); break; } case SdRootFolders: { if (!Value.SetList() || !Tree) return false; for (auto *i = Tree->GetChild(); i; i = i->GetNext()) { ScribeFolder *c = dynamic_cast(i); if (c) { auto p = c->GetPath(); Value.Add(new LVariant(p)); } } break; } default: { return false; } } return true; } bool ScribeWnd::CallMethod(const char *MethodName, LVariant *ReturnValue, LArray &Args) { ScribeDomType m = StrToDom(MethodName); switch (m) { case SdGrowlOnMail: // Type: (Mail Obj) { if (Args.Length() != 1) { LgiTrace("%s:%i - Wrong arg count: %i.\n", _FL, (int)Args.Length()); return false; } Mail *m = dynamic_cast(Args[0]->CastDom()); if (!m) { LgiTrace("%s:%i - Invalid object.\n", _FL); return false; } GrowlOnMail(m); break; } case SdGrowlInfo: { auto Title = Args.Length() > 0 ? Args[0] : NULL; auto Text = Args.Length() > 1 ? Args[1] : NULL; GrowlInfo(Title ? Title->Str() : NULL, Text ? Text->Str() : NULL); break; } case SdGetClipboardText: // Type: () { LClipBoard c(this); *ReturnValue = c.Text(); break; } case SdSetClipboardText: // Type: (String Text) { if (Args.Length() != 1) { LgiTrace("%s:%i - Wrong arg count: %i.\n", _FL, (int)Args.Length()); return false; } char *Str = Args[0]->CastString(); LClipBoard c(this); if (ValidStr(Str)) *ReturnValue = c.Text(Str); else *ReturnValue = c.Empty(); break; } case SdLookupContactGroup: // Type: (String GroupName) { if (Args.Length() != 1) { LgiTrace("%s:%i - Wrong arg count: %i.\n", _FL, (int)Args.Length()); return false; } ContactGroup *Grp = LookupContactGroup(this, Args[0]->Str()); *ReturnValue = dynamic_cast(Grp); break; } case SdGetUserString: // Type: (LView ParentView, String PromptMessage[, Bool ObsurePassword[, String DefaultValue]]) { if (Args.Length() < 2) return false; LView *View = Args[0]->CastView(); char *Prompt = Args[1]->CastString(); bool Pass = Args.Length() > 2 ? Args[2]->CastInt32() != 0 : false; char *Default = Args.Length() > 3 ? Args[3]->CastString() : NULL; LString Result; bool Loop = true; auto i = new LInput(View ? View : this, Default, Prompt, AppName, Pass); i->DoModal([&Result, &Loop, i](auto dlg, auto id) { if (id) Result = i->GetStr(); delete dlg; Loop = false; }); // This is obviously not ideal, but I don't want to implement a scripting language callback for // something that should be a simple modal dialog that waits for user input. while (Loop) { LSleep(10); LYield(); } if (ReturnValue) *ReturnValue = Result; break; } case SdCreateAccount: // Type: () { ScribeAccount *a = new ScribeAccount(this, (int)Accounts.Length()); if (a) { if (ReturnValue) *ReturnValue = (LDom*)a; Accounts.Insert(a); a->Create(); } else return false; break; } case SdDeleteAccount: // Type: (ScribeAccount AccountToDelete) { if (Args.Length() != 1) return false; ScribeAccount *a = dynamic_cast(Args[0]->CastDom()); if (!a) { if (ReturnValue) *ReturnValue = false; } else { int Idx = (int)Accounts.IndexOf(a); if (Idx < 0 || a->IsOnline()) { if (ReturnValue) *ReturnValue = false; } else { Accounts.Delete(a); a->Delete(); delete a; // delete actual account object // Reindex remaining items so their are no gaps int i=0; auto it = Accounts.begin(); for (a = *it; a; a = *++it, i++) { a->ReIndex(i); } } } break; } case SdShowRemoteContent: { if (PreviewPanel) return PreviewPanel->CallMethod(MethodName, ReturnValue, Args); else return false; break; } case SdSearchHtml: // Type(Html, SearchExp, ResultExp) { if (Args.Length() != 3) { LgiTrace("%s:%i - SearchHtml requires 3 arguments.\n", _FL); *ReturnValue = false; return true; } auto Html = Args[0]->Str(); auto SearchExp = Args[1]->Str(); auto ResultExp = Args[2]->Str(); if (!Html || !SearchExp || !ResultExp) { LgiTrace("%s:%i - SearchHtml got non-string argument.\n", _FL); *ReturnValue = false; return true; } SearchHtml(ReturnValue, Html, SearchExp, ResultExp); return true; } case SdGetUri: // Type(UriToDownload, CallbackName) { if (Args.Length() < 2) { LgiTrace("%s:%i - GetUri requires at least 2 arguments.\n", _FL); *ReturnValue = false; return true; } auto Uri = Args[0]->Str(); auto Callback = Args[1]->Str(); LVariant *UserData = Args.Length() > 2 ? Args[2] : NULL; new ScriptDownloadContentThread(this, Uri, Callback, UserData); *ReturnValue = true; return true; } default: { LAssert(!"Unsupported method."); return false; } } return true; } LOptionsFile *ScribeWnd::GetOptions(bool Create) { if (!d->Options && Create) { LAssert(!"Not here... do it in LoadOptions."); return NULL; } return d->Options; } int OptionsFileCmp(OptionsInfo *a, OptionsInfo *b) { int64 Diff = b->Mod - a->Mod; return Diff < 0 ? -1 : (Diff > 0 ? 1 : 0); } OptionsInfo::OptionsInfo() { Score = 0; Mod = 0; Leaf = NULL; Usual = false; } OptionsInfo &OptionsInfo::operator =(char *p) { File = p; Leaf = LGetLeaf(File); if (Leaf) { char n[64]; sprintf_s(n, sizeof(n), "%s.xml", OptionsFileName); Usual = !_stricmp(n, Leaf); } return *this; } LOptionsFile *OptionsInfo::Load() { // Read the file... LAutoPtr Of(new LOptionsFile(File)); if (!Of) return NULL; if (!Of->SerializeFile(false)) return NULL; // Sanity check the options... LXmlTag *Acc = Of->LockTag(OPT_Accounts, _FL); if (!Acc) return NULL; Of->Unlock(); LXmlTag *Stores = Of->LockTag(OPT_MailStores, _FL); if (!Stores) return NULL; auto Count = Stores->Children.Length(); Of->Unlock(); if (Count == 0) Of.Reset(); return Of.Release(); } #define DEBUG_OPTS_SCAN 0 bool ScribeWnd::ScanForOptionsFiles(LArray &Files, LSystemPath PathType) { LString Root = LGetSystemPath(PathType); LDirectory Dir; char p[MAX_PATH_LEN]; if (IsUnitTest) Root += ".unittest"; #if DEBUG_OPTS_SCAN LgiTrace("%s:%i - Root='%s'\n", _FL, Root.Get()); #endif for (int b = Dir.First(Root); b; b = Dir.Next()) { if ( !Dir.IsDir() && Dir.Path(p, sizeof(p)) ) { LResolveShortcut(p, p, sizeof(p)); char *Ext = LGetExtension(Dir.GetName()); if (stristr(Dir.GetName(), OptionsFileName) != NULL && Ext && (!_stricmp(Ext, "xml") || !_stricmp(Ext, "bak"))) { OptionsInfo &i = Files.New(); i = p; i.Mod = Dir.GetLastWriteTime(); #if DEBUG_OPTS_SCAN LgiTrace("%s:%i - File='%s'\n", _FL, p); #endif } } } Files.Sort(OptionsFileCmp); // Scan through the results and pick out the normal file for (unsigned i=0; iOptions; i++) { if (Files[i].Usual) { d->Options = Files[i].Load(); #if DEBUG_OPTS_SCAN LgiTrace("%s:%i - Attempt '%s' = %p\n", _FL, Files[i].File.Get(), d->Options); #endif } } if (!d->Options) { // Scan through the alternative files and look for something // we can use. #if DEBUG_OPTS_SCAN LgiTrace("%s:%i - Scanning backups\n", _FL); #endif for (unsigned i=0; iOptions; i++) { if (!Files[i].Usual) { d->Options = Files[i].Load(); if (d->Options) { // Lets rename this baby back to the real filename LString Xml = OptionsFileName; Xml += ".xml"; LFile::Path Normal(Root, Xml); if (LFileExists(Normal)) FileDev->Delete(Normal); d->Options->SetFile(Normal); d->Options->SerializeFile(true); // sets to clean after changing filename. } } } } if (d->Options) { // Load OK: Clear out any old options files... #if DEBUG_OPTS_SCAN LgiTrace("%s:%i - Files.len=" LPrintfSizeT "\n", _FL, Files.Length()); #endif while (Files.Length() > 6) { auto Idx = Files.Length() - 1; auto &f = Files[Idx]; #if DEBUG_OPTS_SCAN LgiTrace("%s:%i - Delete '%s'\n", _FL, f.File.Get()); #endif FileDev->Delete(f.File); Files.DeleteAt(Idx); } } return d->Options != NULL; } bool ScribeWnd::IsUnitTest = false; bool ScribeWnd::LoadOptions() { bool Load = false; // Check if we are running unit tests... if ((IsUnitTest = LAppInst->GetOption("unittest"))) { d->UnitTestServer.Reset(new LUnitTestServer(this)); } // Look in the XGate folder #if WINNATIVE && !defined(_DEBUG) LRegKey xgate(false, "HKEY_CURRENT_USER\\Software\\XGate"); if (xgate.IsOk()) { char *Spool = xgate.GetStr("SpoolDir"); if (LDirExists(Spool)) { char File[MAX_PATH_LEN]; LMakePath(File, sizeof(File), Spool, "spool"); LMakePath(File, sizeof(File), File, OptionsFileName); strcat_s(File, sizeof(File), ".xml"); if (LFileExists(File)) { d->SetInstallMode(LOptionsFile::DesktopMode); LgiTrace("Selecting xgate mode based on options file path.\n"); d->Options = new LOptionsFile(File); } } } #endif // Now look in the application install folder LArray Files; if (!d->Options && ScanForOptionsFiles(Files, LSP_APP_INSTALL)) { // File is in the install folder... d->SetInstallMode(LOptionsFile::PortableMode); LgiTrace("Selecting portable mode based on options file path.\n"); } // Look in the app root if ( !d->Options && ScanForOptionsFiles(Files, LSP_APP_ROOT) ) { // Desktop mode d->SetInstallMode(LOptionsFile::DesktopMode); LgiTrace("Selecting desktop mode based on options file path.\n"); } // Do multi-instance stuff if (d->Options && d->Options->GetFile()) { // Search for other instances of Scribe if (ScribeIpc) { int i; ScribeIpcInstance *InstLst = (ScribeIpcInstance*) ScribeIpc->GetPtr(); ScribeIpcInstance *Mul = 0; for (i=0; iMulPassword = Mul->Password; break; } } for (i=0; iIsScribe(), InstLst->Magic, InstLst->Pid); if (InstLst->IsScribe() && InstLst != ThisInst) { // LgiTrace("%s, %s\n", InstLst->OptionsPath, d->Options->GetFile()); if (Mul || _stricmp(InstLst->OptionsPath, d->Options->GetFile()) == 0) { int Pid = InstLst->Pid; OsAppArguments *Args = LAppInst->GetAppArgs(); if (!LIsProcess(Pid)) { continue; } char *Utf8 = 0; #if WINNATIVE Utf8 = WideToUtf8(Args->lpCmdLine); #else LStringPipe p; for (int i=1; iArgs; i++) { if (i > 1) p.Push(" "); char *Sp = strchr(Args->Arg[i], ' '); if (Sp) p.Push("\""); p.Push(Args->Arg[i]); if (Sp) p.Push("\""); } Utf8 = p.NewStr(); #endif if (Utf8) { size_t Len = strlen(Utf8); if (Len > 255) { InstLst->Flags |= SCRIBE_IPC_LONG_ARGS; InstLst->Flags &= ~SCRIBE_IPC_CONTINUE_ARGS; for (char *u = Utf8; Len > 0; u += 255) { ssize_t Part = MIN(sizeof(InstLst->Args)-1, Len); memcpy(InstLst->Args, u, Part); Len -= Part; int64 Start = LCurrentTime(); while (LCurrentTime() - Start < 60000) { if (TestFlag(InstLst->Flags, SCRIBE_IPC_CONTINUE_ARGS) && InstLst->Args[0] == 0) { Start = 0; break; } LSleep(10); } if (Start) { LgiTrace("%s:%i - SendLA timed out.\n", _FL); break; } } InstLst->Flags &= ~(SCRIBE_IPC_CONTINUE_ARGS | SCRIBE_IPC_LONG_ARGS); } else { strcpy_s(InstLst->Args, sizeof(InstLst->Args), Utf8); } DeleteArray(Utf8); ShutdownIpc(); LgiTrace("Passed args to the other running instance of Scribe (pid=%i)\n", Pid); LCloseApp(); return false; } else LgiTrace("%s:%i - No arguments to pass.\n", _FL); } } } if (Mul && LFileExists(Mul->OptionsPath)) { // No instance of Scribe is running, but MUL may be keeping // the previously run instance around. So we should run that // by using the options file and password from MUL's instance // record. DeleteObj(d->Options); d->Options = new LOptionsFile(Mul->OptionsPath); } } // Insert ourselves into the instance list if (ThisInst) { strcpy_s(ThisInst->OptionsPath, sizeof(ThisInst->OptionsPath), d->Options->GetFile()); } } // Open file and load.. if (!Load && d->Options) { LOptionsFile *Opts = GetOptions(); Load = Opts->SerializeFile(false); if (Load) { LVariant v = d->GetInstallMode() == LOptionsFile::PortableMode; GetOptions()->SetValue(OPT_IsPortableInstall, v); } else { auto err = GetOptions()->GetError(); LgiMsg( this, LLoadString(IDS_ERROR_LR8_FAILURE), AppName, MB_OK, err); } } if (!d->Options) { // d->Options = new LOptionsFile(d->GetInstallMode(), OptionsFileName); return false; } if (d->Options) { LVariant v; if (!d->Options->GetValue(OPT_IsPortableInstall, v) && d->GetInstallMode() != LOptionsFile::UnknownMode) { v = d->GetInstallMode() == LOptionsFile::PortableMode; d->Options->SetValue(OPT_IsPortableInstall, v); } ScribeOptionsDefaults(d->Options); if (Load) { if (GetOptions()->GetValue(OPT_PrintSettings, v)) { auto *p = GetPrinter(); if (p) { LString s = v.Str(); p->Serialize(s, false); } } } if (d->Options->GetValue(OPT_PreviewLines, v)) { Mail::PreviewLines = v.CastInt32() != 0; } // upgrade smtp password const char *Pw = "SmtpPsw"; if (!GetOptions()->GetValue(OPT_EncryptedSmtpPassword, v)) { // no encrypted password, look for unencrypted password if (GetOptions()->GetValue(Pw, v)) { GPassword p; p.Set(v.Str()); p.Serialize(GetOptions(), OPT_EncryptedSmtpPassword, true); } } // if old un-encrypted password exists... // delete the key, we are now storing an encrypted // password if (GetOptions()->GetValue(Pw, v)) GetOptions()->DeleteValue(Pw); if (GetOptions()->GetValue(OPT_AdjustDateTz, v)) Mail::AdjustDateTz = !v.CastInt32(); if (!GetOptions()->GetValue(OPT_ConfirmDelete, v)) GetOptions()->SetValue(OPT_ConfirmDelete, v = true); if (!GetOptions()->GetValue(OPT_DelDirection, v)) GetOptions()->SetValue(OPT_DelDirection, v = DeleteActionPrev); if (GetOptions()->GetValue(OPT_SizeInKiB, v)) OptionSizeInKiB = v.CastInt32() != 0; if (GetOptions()->GetValue(OPT_RelativeDates, v)) ShowRelativeDates = v.CastInt32() != 0; // date format if (GetOptions()->GetValue(OPT_DateFormat, v)) { int Idx = v.CastInt32(); if (Idx >= 0 && Idx < CountOf(DateTimeFormats)) LDateTime::SetDefaultFormat(DateTimeFormats[Idx]); } // SSL debug logging if (GetOptions()->GetValue(OPT_DebugSSL, v)) SslSocket::DebugLogging = v.CastInt32() != 0; // Growl if (GetOptions()->GetValue(OPT_GrowlEnabled, v) && v.CastInt32()) { LVariant Ver, Bld; GetVariant(DomToStr(SdVersion), Ver); GetVariant("Build", Bld); LString n; n.Printf("%s\n%s", Ver.Str(), Bld.Str()); GrowlInfo("Scribe has started up...", n); } } #if LGI_EXCEPTIONS try { #endif // Default the font settings to the system font // if they don't already exist const char *OptFont[] = { OPT_EditorFont, OPT_PrintFont, OPT_HtmlFont, 0 }; int Index = 0; for (const char **Opt=OptFont; *Opt; Opt++, Index++) { LVariant v; if (!GetOptions()->GetValue(*Opt, v)) { LFontType Type; if (Type.GetSystemFont("System")) { if (Index == 2) { int Pt = Type.GetPointSize(); Type.SetPointSize(Pt+3); } Type.Serialize(GetOptions(), *Opt, true); } } } #if LGI_EXCEPTIONS } catch (...) { LgiMsg( this, LLoadString(IDS_ERROR_FONT_SETTINGS), AppName, MB_OK); } #endif return true; } bool ScribeWnd::SaveOptions() { LStringPipe Log(256); bool Status = false; bool WriteFailed = false; bool WndStateSet = false; RestartSave: if (d->Options && !d->Options->GetFile()) { bool PortableOk = true; char Path[MAX_PATH_LEN]; char Leaf[32]; sprintf_s(Leaf, sizeof(Leaf), "%s.xml", OptionsFileName); Log.Print("No current path for '%s', creating...\n", Leaf); LVariant v; GetOptions()->GetValue(OPT_IsPortableInstall, v); if (v.CastInt32()) { if (!LGetSystemPath(LSP_APP_INSTALL, Path, sizeof(Path))) { PortableOk = false; Log.Print("Error: LgiGetSystemPath(LSP_APP_INSTALL) failed.\n"); } else { LMakePath(Path, sizeof(Path), Path, Leaf); // Do write test to confirm we are good to go LFile f; if (f.Open(Path, O_WRITE)) { f.Close(); FileDev->Delete(Path, false); d->Options->SetFile(Path); } else { PortableOk = false; Log.Print("Warning: '%s' is not writable.\n", Path); } } } if (!v.CastInt32() || !PortableOk) { // Desktop mode then. if (v.CastInt32()) { const char *Msg = "Switching to desktop mode because the install folder is not writable."; Log.Print("%s\n", Msg); LgiMsg(this, Msg, AppName, MB_OK); GetOptions()->SetValue(OPT_IsPortableInstall, v = false); } if (!LGetSystemPath(LSP_APP_ROOT, Path, sizeof(Path))) { Log.Print("Error: LgiGetSystemPath(LSP_APP_ROOT) failed.\n"); } else { LMakePath(Path, sizeof(Path), Path, LAppInst->LBase::Name()); if (!LDirExists(Path)) { if (!FileDev->CreateFolder(Path)) { Log.Print("Error: CreateFolder('%s') failed.\n", Path); } } LMakePath(Path, sizeof(Path), Path, Leaf); // Do write test to confirm we are good to go LFile f; if (f.Open(Path, O_WRITE)) { f.Close(); FileDev->Delete(Path, false); d->Options->SetFile(Path); } else { Log.Print("Error: '%s' is not writable.\n", Path); } } } } if (d->Options && d->Options->GetFile() && d->Options->IsValid()) { // Backup options file char Backup[MAX_PATH_LEN]; strcpy_s(Backup, sizeof(Backup), d->Options->GetFile()); char *Ext = LGetExtension(Backup); if (Ext) { *--Ext = 0; LString s; for (int i=1; i<100; i++) { s.Printf("%s_%i.bak", Backup, i); if (!LFileExists(s)) break; } if (!LFileExists(s)) FileDev->Move(d->Options->GetFile(), s); } // Update some settings... #if LGI_VIEW_HANDLE if (Handle()) #endif WndStateSet = SerializeState(GetOptions(), OPT_ScribeWndPos, false); LVariant v; if (Splitter) GetOptions()->SetValue(OPT_SplitterPos, v = (int)Splitter->Value()); if (d->SubSplit) { auto First = d->SubSplit->GetViewAt(0); if (First == (LViewI*)SearchView) { auto Lst = (SearchView) ? d->SubSplit->GetViewAt(1) : NULL; if (Lst) GetOptions()->SetValue(OPT_SubSplitPos, v = (int)Lst->GetPos().Y()); } else GetOptions()->SetValue(OPT_SubSplitPos, v = (int)d->SubSplit->Value()); } // Write them... if (GetOptions()->SerializeFile(true)) { Status = true; } else { // We probably don't have write permissions to the install folder... Log.Print("Error: Options.Serialize failed.\n"); if (!WriteFailed) { // This blocks any possibility of an infinite loop WriteFailed = true; d->Options->SetFile(NULL); // Set desktop mode explicitly LVariant v; GetOptions()->GetValue(OPT_IsPortableInstall, v = false); Log.Print("Restarting save after setting desktop mode...\n"); goto RestartSave; } } } if (!Status) { LString a = Log.NewLStr(); LgiMsg(this, "Saving options failed:\n%s", AppName, MB_OK, a.Get()); } if (!WndStateSet) { LRect r(10, 10, 790, 590); SetPos(r); MoveToCenter(); } return Status; } // // Command Line Options: // // -m, -t : To recipient(s) // -f : The filename of the attachment // -b : Attach as a binary // -c : CC'd recipient(s) // -s : Subject for the email // -n : Send now... else UI is shown // -p : Print the file // -upgrade_folders : trigger a folder upgrade // -o : Load the following options file // -u : Load the following URL/file // void ScribeWnd::OnCommandLine() { // check command line args LString Str, File; bool CreateMail = false; CreateMail = LAppInst->GetOption("m", Str); if (!CreateMail) CreateMail = LAppInst->GetOption("t", Str); bool HasFile = LAppInst->GetOption("f", File); if (!CreateMail) CreateMail = HasFile; LString OpenArg; if (LAppInst->GetOption("u", OpenArg)) { LUri u(OpenArg); if (u.sProtocol) { OnUrl(OpenArg); } else if (LFileExists(OpenArg)) { LArray Files; Files.Add(OpenArg); OnReceiveFiles(Files); } } Mail *NewEmail = 0; if (CreateMail && Str) { // strip off quotes if needed char *In = Str, *Out = Str; for (; In && *In; In++) { if (!strchr("\'\"", *In)) { *Out++ = *In; } } *Out++ = 0; // create object NewEmail = dynamic_cast(CreateItem(MAGIC_MAIL, NULL, false)); if (NewEmail) { Mailto mt(this, Str); mt.Apply(NewEmail); // cc's? if (LAppInst->GetOption("c", Str)) { SetRecipients(this, Str, NewEmail->GetObject()->GetList(FIELD_TO), MAIL_ADDR_CC); } // attach a file? if (File) { if (LAppInst->GetOption("b")) { // attach as a binary file NewEmail->AttachFile(this, &File[0]); } else { // insert as the body LAutoString b(LReadTextFile(&File[0])); if (b) { NewEmail->SetBody(b); } } } // subject? if (LAppInst->GetOption("s", Str)) { NewEmail->SetSubject(Str); } // Send now or later? if (LAppInst->GetOption("n")) { // Check for exit after send option d->ExitAfterSend = LAppInst->GetOption("exit"); // now NewEmail->SetFlags(MAIL_CREATED | MAIL_READY_TO_SEND | NewEmail->GetFlags()); NewEmail->Save(); OnCommand(IDM_SEND_MAIL, 0, #ifndef __GTK_H__ Handle() #else NULL #endif ); } else { // later NewEmail->DoUI(); } } } // Pop3 on startup option LVariant n; if (GetOptions()->GetValue(OPT_Pop3OnStart, n) && n.CastInt32()) { OnCommand(IDM_RECEIVE_MAIL, 0, NULL); } } void ScribeWnd::SetCurrentIdentity(int i) { LVariant v = i; GetOptions()->SetValue(OPT_CurrentIdentity, v); if (DefaultIdentityItem) DefaultIdentityItem->Checked(i < 0); for (auto a: Accounts) { a->SetCheck(i == a->GetIndex()); } } ScribeAccount *ScribeWnd::GetCurrentAccount() { auto Idx = GetCurrentIdentity(); ScribeAccount *a = (Idx >= 0 && Idx < (ssize_t)Accounts.Length()) ? Accounts.ItemAt(Idx) : NULL; bool ValidId = a != NULL && a->IsValid(); if (!ValidId) { LAssert(!"No current identity?"); // Find a valid account to be the identity... for (auto a : Accounts) { if (!a->Send.Disabled() && a->Identity.IsValid()) { break; } } } return a; } int ScribeWnd::GetCurrentIdentity() { LVariant i; if (GetOptions()->GetValue(OPT_CurrentIdentity, i)) return i.CastInt32(); else if (ScribeState != ScribeInitializing) LgiTrace("%s:%i - No OPT_CurrentIdentity set.\n", _FL); return -1; } void ScribeWnd::SetupAccounts() { int i, CurrentIdentity = GetCurrentIdentity(); if (StatusPanel) { StatusPanel->Empty(); } #if !defined(COCOA) // FIXME LAssert(ReceiveMenu && PreviewMenu); #endif if (SendMenu) SendMenu->Empty(); if (ReceiveMenu) ReceiveMenu->Empty(); if (PreviewMenu) PreviewMenu->Empty(); if (IdentityMenu) { IdentityMenu->Empty(); } static bool Startup = true; bool ResetDefault = false; LArray Enabled; for (i=0; true; i++) { // char *s = 0; ScribeAccount *a = Startup ? new ScribeAccount(this, i) : Accounts[i]; if (a) { if (i == 0) { a->Create(); } a->Register(this); LVariant ReceiveName = a->Receive.Name(); LVariant ReceiveServer = a->Receive.Server(); LVariant SendServer = a->Send.Server(); if (i == 0 || ValidStr(ReceiveName.Str()) || ValidStr(ReceiveServer.Str()) || ValidStr(SendServer.Str()) ) { a->Send.SendItem = SendItem; a->Receive.ReceiveItem = ReceiveItem; a->Receive.PreviewItem = PreviewItem; if (!Accounts.HasItem(a)) { Accounts.Insert(a); } if (i) a->Create(); a->InitMenus(); // Identity Menu Item LVariant IdEmail = a->Identity.Email(); LVariant IdName = a->Identity.Name(); if (IdentityMenu && ValidStr(IdEmail.Str())) { char s[256]; if (IdName.Str()) sprintf_s(s, sizeof(s), "%s <%s>", IdName.Str(), IdEmail.Str()); else sprintf_s(s, sizeof(s), "<%s>", IdEmail.Str()); a->SetMenuItem(IdentityMenu->AppendItem(s, IDM_IDENTITY_BASE+i+1, !a->Send.Disabled())); if (a->Send.Disabled()) { a->SetCheck(false); if (i == CurrentIdentity) ResetDefault = true; } else { a->SetCheck(i == CurrentIdentity); Enabled[i] = a; } } } else { Accounts.Delete(a); DeleteObj(a); } } if (!a) break; } if ((ResetDefault || CurrentIdentity < 0) && Enabled.Length()) { for (unsigned i=0; iSetCheck(true); LVariant v; GetOptions()->SetValue(OPT_CurrentIdentity, v = (int)i); break; } } } Startup = false; if (ReceiveMenu && i == 0) { ReceiveMenu->AppendItem(LLoadString(IDS_NO_ITEMS), 0, false); } if (StatusPanel) { StatusPanel->OnAccountListChange(); } SetPulse(100); } ////////////////////////////////////////////////////////////////////////////// class LShutdown : public LDialog { LTextLabel *Msg; LButton *KillBtn; LButton *CancelBtn; bool Disconnected; public: ScribeAccount *Wait; List *Accounts; LShutdown(List *accounts) { Wait = 0; Disconnected = false; Accounts = accounts; LRect r( 0, 0, 320 + LAppInst->GetMetric(LGI_MET_DECOR_X), 70 + LAppInst->GetMetric(LGI_MET_DECOR_Y)); SetPos(r); MoveToCenter(); char Str[256]; sprintf_s(Str, sizeof(Str), "%s exiting...", AppName); LView::Name(Str); AddView(Msg = new LTextLabel(-1, 10, 10, 300, -1, "None")); AddView(KillBtn = new LButton(IDC_KILL, 70, 35, 60, 20, "Kill")); AddView(CancelBtn = new LButton(IDCANCEL, 140, 35, 60, 20, "Cancel")); if (KillBtn) { KillBtn->Enabled(false); } } void OnCreate() { SetPulse(100); } void OnPulse() { if (Accounts) { if (!Wait) { Wait = (*Accounts)[0]; if (Wait) { Disconnected = false; char s[256]; LVariant v = Wait->Receive.Name(); sprintf_s(s, sizeof(s), "Waiting for '%s'", v.Str() ? v.Str() : (char*)"Untitled..."); Msg->Name(s); Accounts->Delete(Wait); Wait->Stop(); KillBtn->Enabled(true); } else { SetPulse(); EndModal(true); } } if (Wait && !Wait->IsOnline()) { Wait = 0; Msg->Name("None"); KillBtn->Enabled(false); } } else { SetPulse(); EndModal(false); } } int OnNotify(LViewI *Ctrl, LNotification n) { switch (Ctrl->GetId()) { case IDC_KILL: { if (Wait) { if (!Disconnected) { Disconnected = true; Wait->Disconnect(); } else { Wait->Kill(); } } break; } case IDCANCEL: { EndModal(false); break; } } return 0; } }; bool ScribeWnd::OnRequestClose(bool OsShuttingDown) { if (FolderTasks.Length() > 0) { LgiTrace("%s:%i - %i folder tasks still busy...\n", _FL, FolderTasks.Length()); return false; } LString OnClose = LAppInst->GetConfig("Scribe.OnClose"); if (!d->IngoreOnClose && !OsShuttingDown && !Stricmp(OnClose.Get(), "minimize")) { SetZoom(LZoomMin); return false; } Visible(false); if (ScribeState != ScribeRunning) { // Inside a folder load/unload or initialization // Tell the loader to quit out... ScribeState = ScribeExiting; // Leave now, we can exit when we're ready return false; } else if (IsSending() || GetActiveThreads() > 0) { // whack up a shutdown window List Online; for (auto i: Accounts) { i->OnEndSession(); if (i->IsOnline()) { Online.Insert(i); } } auto Dlg = new LShutdown(&Online); Dlg->DoModal([this, Dlg](auto dlg, auto id) { if (id) { ScribeState = ScribeExiting; LCloseApp(); } else { ScribeState = ScribeRunning; Visible(true); } }); return false; // At the very minimum the app has to wait for the user to respond. } else { // End all sessions if any... for (auto i: Accounts) { i->OnEndSession(); } } // close all the other top level windows while (ThingUi::All.Length() > 0) { ThingUi *Ui = ThingUi::All.First(); if (!Ui->OnRequestClose(OsShuttingDown)) { ScribeState = ScribeRunning; Visible(true); return false; } size_t Start = ThingUi::All.Length(); Ui->Quit(); if (ThingUi::All.Length() >= Start) { LAssert(0); break; } } SerializeState(GetOptions(), OPT_ScribeWndPos, false); LCloseApp(); return LWindow::OnRequestClose(OsShuttingDown); } void ScribeWnd::DoOnTimer(LScriptCallback *c) { if (!c) return; auto Now = LCurrentTime(); if (c->PrevTs) { auto Since = Now - c->PrevTs; double Sec = (double)Since / 1000.0; if (Sec >= c->fParam) { // Call the function c->PrevTs = Now; LVirtualMachine Vm; LScriptArguments Args(&Vm); LVariant This((LDom*)this); Args.Add(&This); ExecuteScriptCallback(*c, Args); } } else { c->PrevTs = Now; } } void ScribeWnd::OnMinute() { if (Folders.Length() == 0) return; // Check for calendar event alarms... Calendar::CheckReminders(); // Check for any outgoing email that should be re-attempted... ScribeFolder *Outbox = GetFolder(FOLDER_OUTBOX); if (Outbox) { bool Resend = false; for (auto t : Outbox->Items) { Mail *m = t->IsMail(); if (m && !TestFlag(m->GetFlags(), MAIL_SENT) && TestFlag(m->GetFlags(), MAIL_READY_TO_SEND) && m->SendAttempts > 0) { Resend = true; break; } } if (Resend) Send(); } LArray Cb; if (GetScriptCallbacks(LOnTimer, Cb)) { for (auto c: Cb) { if (!c->Func) continue; if (c->fParam == 0.0) { // Work out the period from 'Data' char *s = c->Data.Str(); while (*s && IsWhiteSpace(*s)) s++; char *u = s; while (*u && !IsAlpha(*u)) u++; double v = atof(s); switch (*u) { case 's': case 'S': // seconds c->fParam = v; break; case 'm': case 'M': // mins c->fParam = v * LDateTime::MinuteLength; break; case 'h': case 'H': // hours c->fParam = v * LDateTime::HourLength; break; case 'd': case 'D': // days c->fParam = v * LDateTime::DayLength; break; default: { LgiTrace("%s:%i - Couldn't understand period '%s'\n", _FL, c->Data.Str()); c->Data.Empty(); break; } } if ((c->OnSecond = c->fParam < 60.0)) { d->OnSecondTimerCallbacks.Add(c); } } if (!c->OnSecond) DoOnTimer(c); } } } void ScribeWnd::OnHour() { // Force time zone update in case of daylight savings change. LDateTime::SystemTimeZone(true); // Check if we need should be doing a software update check static bool InSoftwareCheck = false; if (!InSoftwareCheck) { char s[64]; InSoftwareCheck = true; LVariant v; if (GetOptions()->GetValue(OPT_SoftwareUpdate, v) && v.CastInt32()) { LDateTime Now, Last; Now.SetFormat(GDTF_YEAR_MONTH_DAY); Last.SetFormat(Now.GetFormat()); Now.SetNow(); if (!GetOptions()->GetValue(OPT_SoftwareUpdateLast, v) || !Last.Set(v.Str())) { // Record now as the last check point Now.Get(s, sizeof(s)); GetOptions()->SetValue(OPT_SoftwareUpdateLast, v = s); } else if (GetOptions()->GetValue(OPT_SoftwareUpdateTime, v)) { // Valid last check date/time. switch (v.CastInt32()) { case 0: // Week Last.AddDays(7); break; case 1: // Month Last.AddMonths(1); break; case 2: // Year Last.AddMonths(12); break; default: LgiTrace("%s:%i - The option '%s' is not valid\n", _FL, OPT_SoftwareUpdateTime); return; } if (Last < Now) { // Save the last date for next time... Now.Get(s, sizeof(s)); GetOptions()->SetValue(OPT_SoftwareUpdateLast, v = s); // Check for update now... LSoftwareUpdate::UpdateInfo Info; GetOptions()->GetValue(OPT_SoftwareUpdateIncBeta, v); IsSoftwareUpToDate(Info, this, false, v.CastInt32() != 0, [Info, this](auto s) { if (s == SwOutOfDate) if (UpgradeSoftware(Info, this, true)) LCloseApp(); }); } } } InSoftwareCheck = false; } } bool ScribeWnd::SaveDirtyObjects(int TimeLimitMs) { bool Status = false; if (Thing::DirtyThings.Length() > 0) { static bool SavingObjects = false; if (!SavingObjects) { SavingObjects = true; LArray WriteTimes; // ssize_t StartDirty = Thing::DirtyThings.Length(); uint64 Start = LCurrentTime(); for (unsigned i=0; iSave(NULL)) { WriteTimes.Add((int) (LCurrentTime() - WriteStart)); LAssert(!ThingType::DirtyThings.HasItem(t)); Status = true; } else { LgiTrace("Failed to save thing type 0x%x\n", t->Type()); FailedWrites++; if (FailedWrites > 2) { while (ThingType::DirtyThings.Length()) ThingType::DirtyThings[0]->SetDirty(false); FailedWrites = 0; } } } } SavingObjects = false; /* if (Status) { LStringPipe p; p.Print("WriteTimes: "); for (unsigned i=0; iLastTs >= 1000) { d->LastTs = Now; OnPulseSecond(); } } } void ScribeWnd::OnPulseSecond() { #if PROFILE_ON_PULSE LProfile Prof("NewMailLst handling"); Prof.HideResultsIfBelow(50); #endif if (Mail::NewMailLst.Length() > 0) { LVariant Blink; if (GetOptions()->GetValue(OPT_BlinkNewMail, Blink) && Blink.CastInt32()) { TrayIcon.Value((TrayIcon.Value() == TRAY_ICON_MAIL) ? TRAY_ICON_NONE : TRAY_ICON_MAIL); } } else { bool Err = false; for (auto a: Accounts) { if (!a->Receive.GetStatus() || !a->Send.GetStatus()) { Err = true; } } TrayIcon.Value(Err ? TRAY_ICON_ERROR : TRAY_ICON_NORMAL); } #if PROFILE_ON_PULSE Prof.Add("StatusPanel handling"); #endif if (StatusPanel) { StatusPanel->OnPulse(); } #if PROFILE_ON_PULSE Prof.Add("OnXXXX handling"); #endif LDateTime Now; Now.SetNow(); if (d->LastMinute != Now.Minutes()) // Check every minute... { d->LastMinute = Now.Minutes(); OnMinute(); } if (d->LastHour != Now.Hours()) // Check every hour... { d->LastHour = Now.Hours(); OnHour(); } { // These timers need to be checked every second... for (auto c: d->OnSecondTimerCallbacks) DoOnTimer(c); } #if PROFILE_ON_PULSE Prof.Add("Instance handling"); #endif if (ThisInst && ValidStr(ThisInst->Args)) { LStringPipe p; p.Push(ThisInst->Args); if (ThisInst->Flags & SCRIBE_IPC_LONG_ARGS) { ThisInst->Flags |= SCRIBE_IPC_CONTINUE_ARGS; int64 Start = LCurrentTime(); while ( TestFlag(ThisInst->Flags, SCRIBE_IPC_LONG_ARGS) && LCurrentTime() - Start < 60000) { ZeroObj(ThisInst->Args); while ( TestFlag(ThisInst->Flags, SCRIBE_IPC_LONG_ARGS) && !ThisInst->Args[0] && LCurrentTime() - Start < 60000) { LSleep(10); } p.Push(ThisInst->Args); } } ZeroObj(ThisInst->Args); LAutoString Arg(p.NewStr()); if (Arg) { OsAppArguments AppArgs(0, 0); LgiTrace("Received cmd line: %s\n", Arg.Get()); AppArgs.Set(Arg); LAppInst->SetAppArgs(AppArgs); if (LAppInst->GetOption("m") && LAppInst->GetOption("f")) ; else LAppInst->OnCommandLine(); OnCommandLine(); if (GetZoom() == LZoomMin) SetZoom(LZoomNormal); Visible(true); } } #if PROFILE_ON_PULSE Prof.Add("PreviewPanel handling"); #endif if (PreviewPanel) { PreviewPanel->OnPulse(); } } void ScribeWnd::AddFolderToMru(char *FileName) { if (FileName) { // read MRU List Files; int i; for (i=0; i<10; i++) { char Key[32]; LVariant f; sprintf_s(Key, sizeof(Key), "FolderMru.%i", i); if (GetOptions()->GetValue(Key, f)) { Files.Insert(NewStr(f.Str())); GetOptions()->DeleteValue(Key); } } // remove FileName if present for (auto f: Files) { if (_stricmp(f, FileName) == 0) { Files.Delete(f); DeleteArray(f); break; } } // insert FileName at the start of the list Files.Insert(NewStr(FileName)); // write MRU for (i=0; i<10; i++) { char *n = Files.ItemAt(i); if (n) { char Key[32]; sprintf_s(Key, sizeof(Key), "FolderMru.%i", i); LVariant f; GetOptions()->SetValue(Key, f = n); } else break; } // Clean up Files.DeleteArrays(); } } bool ScribeWnd::CleanFolders(ScribeFolder *f) { if (!f) return false; if (f->Select()) { f->SerializeFieldWidths(); } for (ScribeFolder *c = f->GetChildFolder(); c; c = c->GetNextFolder()) { CleanFolders(c); } return true; } void ScribeWnd::OnFolderChanged(LDataFolderI *folder) { } bool ScribeWnd::OnFolderTask(LEventTargetI *Ptr, bool Add) { if (Add) { if (FolderTasks.HasItem(Ptr)) { LAssert(!"Can't add task twice."); return false; } FolderTasks.Add(Ptr); return true; } else { if (!FolderTasks.HasItem(Ptr)) { LAssert(!"Item not part of task list."); return false; } FolderTasks.Delete(Ptr); return true; } } LMailStore *ScribeWnd::GetDefaultMailStore() { LMailStore *Def = 0; for (unsigned i=0; i Def->Priority()) { Def = &Folders[i]; } } } } return Def; } bool HasMailStore(LXmlTag *MailStores, char *Name) { for (auto t : MailStores->Children) { char *StoreName = t->GetAttr(OPT_MailStoreName); if (StoreName && Name && !_stricmp(StoreName, Name)) return true; } return false; } LDataStoreI *ScribeWnd::CreateDataStore(const char *_Full, bool CreateIfMissing) { LString Full(_Full); auto Ext = LGetExtension(Full); if (Ext) { if (!_stricmp(Ext, "mail2")) { LgiMsg(this, LLoadString(IDS_MAIL2_DEPRECATED), AppName, MB_OK, Full.Get()); } else if (!_stricmp(Ext, "mail3")) { return OpenMail3(Full, this, CreateIfMissing); } else if (!_stricmp(Ext, "sqlite")) { LTrimDir(Full); return OpenMail3(Full, this, CreateIfMissing); } else { LgiTrace("%s:%i - Not a valid mail store extension: %s\n", _FL, Full.Get()); LAssert(!"Not a valid mail store extension."); } } else LgiTrace("%s:%i - No extension for CreateDataStore: %s\n", _FL, Full.Get()); return NULL; } class MailStoreUpgrade : public LProgressDlg, public LDataPropI { public: ScribeWnd *App = NULL; LDataStoreI *Ds = NULL; int Status = -1; LString Error; MailStoreUpgrade(ScribeWnd *app, LDataStoreI *ds) { App = app; Ds = ds; SetCanCancel(false); SetDescription("Upgrading mail store..."); Ds->Upgrade(this, this, [this](auto status) { Status = status; }); } ~MailStoreUpgrade() { } void OnPulse() override { if (Status >= 0) { EndModal(0); return; } return LProgressDlg::OnPulse(); } LDataPropI &operator =(LDataPropI &p) { LAssert(0); return *this; } Store3Status SetStr(int id, const char *str) override { switch (id) { case Store3UiError: Error = str; break; default: LAssert(!"Impl me."); return Store3Error; break; } return Store3Success; } }; bool ScribeWnd::ProcessFolder(LDataStoreI *&Store, int StoreIdx, char *StoreName) { if (Store->GetInt(FIELD_VERSION) == 0) { // version error LgiMsg(this, LLoadString(IDS_ERROR_FOLDERS_VERSION), AppName, MB_OK, 0, Store->GetInt(FIELD_VERSION)); return false; } if (Store->GetInt(FIELD_READONLY)) { LgiMsg(this, LLoadString(IDS_ERROR_READONLY_FOLDERS), AppName); } // get root item LDataFolderI *Root = Store->GetRoot(); if (!Root) return false; ScribeFolder *&Mailbox = Folders[StoreIdx].Root; Mailbox = new ScribeFolder; if (Mailbox) { Mailbox->App = this; Mailbox->SetObject(Root, false, _FL); Root->SetStr(FIELD_FOLDER_NAME, StoreName); Root->SetInt(FIELD_FOLDER_TYPE, MAGIC_NONE); } #ifdef TEST_OBJECT_SIZE // debug/repair code if (Root->StoreSize != Root->Sizeof()) { SizeErrors[0]++; Root->StoreSize = Root->Sizeof(); if (Root->Object) { Root->Object->StoreDirty = true; } } #endif // Insert the root object and then... Tree->Insert(Mailbox); // Recursively load the rest of the tree { LProfile p("Loadfolders"); Mailbox->LoadFolders(); } // This forces a re-pour to re-order the folders according to their // sort settings. Tree->UpdateAllItems(); if (ScribeState != ScribeExiting) { // Show the tree Mailbox->Expanded(Folders[StoreIdx].Expanded); // Checks the folders for a number of required objects // and creates them if required auto StoreType = Store->GetInt(FIELD_STORE_TYPE); if (StoreType == Store3Sqlite) Validate(&Folders[StoreIdx]); else if (StoreType < 0) LAssert(!"Make sure you impl the FIELD_STORE_TYPE field in the store."); // FIXME // AddFolderToMru(Full); } return true; } bool ScribeWnd::LoadMailStores() { bool Status = false; LXmlTag *MailStores = GetOptions()->LockTag(OPT_MailStores, _FL); if (!MailStores) return false; bool OptionsDirty = false; int StoreIdx = 0; for (auto MailStore: MailStores->Children) { if (!MailStore->IsTag(OPT_MailStore)) continue; // Read the folders.. auto Path = MailStore->GetAttr(OPT_MailStoreLocation); auto ContactUrl = MailStore->GetAttr(OPT_MailStoreContactUrl); auto CalUrl = MailStore->GetAttr(OPT_MailStoreCalendarUrl); if (!Path && !ContactUrl && !CalUrl) { LgiTrace("%s:%i - No mail store path (%i).\n", _FL, StoreIdx); continue; } char *StoreName = MailStore->GetAttr(OPT_MailStoreName); if (!StoreName) { char Tmp[256]; for (int i=1; true; i++) { sprintf_s(Tmp, sizeof(Tmp), "Folders%i", i); if (!HasMailStore(MailStores, Tmp)) break; } MailStore->SetAttr(OPT_MailStoreName, Tmp); StoreName = MailStore->GetAttr(OPT_MailStoreName); OptionsDirty = true; } Folders[StoreIdx].Name = StoreName; if (MailStore->GetAsInt(OPT_MailStoreDisable) > 0) { // LgiTrace("%s:%i - Mail store '%i' is disabled.\n", _FL, StoreIdx); continue; } if (Path) { // Mail3 folders on disk... char Full[MAX_PATH_LEN]; if (LIsRelativePath(Path)) { LMakePath(Full, sizeof(Full), GetOptions()->GetFile(), ".."); LMakePath(Full, sizeof(Full), Full, Path); } else { strcpy_s(Full, sizeof(Full), Path); } LVariant CreateFoldersIfMissing; GetOptions()->GetValue(OPT_CreateFoldersIfMissing, CreateFoldersIfMissing); // Sniff type... char *Ext = LGetExtension(Full); if (!Ext) continue; if (!Folders[StoreIdx].Store) Folders[StoreIdx].Store = CreateDataStore(Full, CreateFoldersIfMissing.CastInt32() != 0); if (!Folders[StoreIdx].Store) { LgiTrace("%s:%i - Failed to create data store for '%s'\n", _FL, Full); continue; } Folders[StoreIdx].Path = Full; } else if (ContactUrl || CalUrl) { // Remove Webdav folders... Folders[StoreIdx].Store = new WebdavStore(this, this, StoreName); } else break; LDataStoreI *&Store = Folders[StoreIdx].Store; auto ex = MailStore->GetAsInt(OPT_MailStoreExpanded); if (ex >= 0) Folders[StoreIdx].Expanded = ex != 0; // check if the mail store requires upgrading... Store3Status MsState = (Store3Status)Store->GetInt(FIELD_STATUS); if (MsState == Store3UpgradeRequired) { const char *Details = Store->GetStr(FIELD_STATUS); if (LgiMsg(this, LLoadString(IDS_MAILSTORE_UPGRADE_Q), AppName, MB_YESNO, Folders[StoreIdx].Path.Get(), ValidStr(Details)?Details:"n/a") == IDYES) { auto Prog = new MailStoreUpgrade(this, Store); Prog->DoModal(NULL); } else { continue; } } else if (MsState == Store3Error) { auto ErrMsg = Store->GetStr(FIELD_ERROR); auto a = new LAlert(this, AppName, ErrMsg ? ErrMsg : LLoadString(IDS_ERROR_FOLDERS_STATUS), LLoadString(IDS_EDIT_MAIL_STORES), LLoadString(IDS_OK)); a->DoModal([this](auto dlg, auto Btn) { if (Btn == 1) PostEvent(M_COMMAND, IDM_MANAGE_MAIL_STORES); delete dlg; }); continue; } // check password LString FolderPsw; if ((FolderPsw = Store->GetStr(FIELD_STORE_PASSWORD))) { bool Verified = false; if (ValidStr(d->MulPassword)) { Verified = d->MulPassword.Equals(FolderPsw, false); d->MulPassword.Empty(); } if (!Verified) { auto Dlg = new LInput(this, "", LLoadString(IDS_ASK_FOLDER_PASS), AppName, true); Dlg->DoModal([this, Dlg, FolderPsw, &Store, StoreIdx, StoreName](auto dlg, auto id) { if (id == IDOK) { GPassword User; User.Set(Dlg->GetStr()); if (Dlg->GetStr() == FolderPsw) ProcessFolder(Store, StoreIdx, StoreName); else DeleteObj(Store); } delete dlg; }); } } else ProcessFolder(Store, StoreIdx, StoreName); Status = true; StoreIdx++; } if (Status) { // Force load some folders... ScribeFolder *Folder = GetFolder(FOLDER_CALENDAR); if (Folder) Folder->LoadThings(); Folder = GetFolder(FOLDER_FILTERS); if (Folder) Folder->LoadThings(); for (auto ms: Folders) { if (!ms.Root) continue; for (auto c = ms.Root->GetChildFolder(); c; c = c->GetNextFolder()) { if (c->GetItemType() == MAGIC_CONTACT || c->GetItemType() == MAGIC_FILTER) c->LoadThings(); } } List c; GetContacts(c); // Set selected folder to Inbox by default // if the user hasn't selected a folder already if (ScribeState != ScribeExiting && Tree && !Tree->Selection()) { LVariant StartInFolder; GetOptions()->GetValue(OPT_StartInFolder, StartInFolder); ScribeFolder *Start = NULL; if (ValidStr(StartInFolder.Str())) { Start = GetFolder(StartInFolder.Str()); } if (!Start) { Start = GetFolder(FOLDER_INBOX); } if (Start && Tree) { Tree->Select(Start); } } } GetOptions()->DeleteValue(OPT_CreateFoldersIfMissing); if (OptionsDirty) SaveOptions(); GetOptions()->Unlock(); // Set system folders ScribeFolder *f = GetFolder(FOLDER_INBOX); if (f) f->SetSystemFolderType(Store3SystemInbox); f = GetFolder(FOLDER_OUTBOX); if (f) f->SetSystemFolderType(Store3SystemOutbox); f = GetFolder(FOLDER_SENT); if (f) f->SetSystemFolderType(Store3SystemSent); f = GetFolder(FOLDER_SPAM); if (f) f->SetSystemFolderType(Store3SystemSpam); return Status; } void ScribeWnd::LoadFolders(std::function Callback) { AppState PrevState = ScribeState; ScribeState = ScribeLoadingFolders; // Setup Mailstores tag { LXmlTag *MailStores = GetOptions()->LockTag(OPT_MailStores, _FL); if (!MailStores) { // Check if we can upgrade the old folder tag char n[32]; sprintf_s(n, sizeof(n), "%s-Folders", LGetOsName()); LVariant OldFolders; GetOptions()->GetValue(n, OldFolders); // Create mail store element.. GetOptions()->CreateTag(OPT_MailStores); if ((MailStores = GetOptions()->LockTag(OPT_MailStores, _FL))) { if (OldFolders.Str()) { LXmlTag *Store = MailStores->CreateTag(OPT_MailStore); if (Store) { char Opts[MAX_PATH_LEN]; LMakePath(Opts, sizeof(Opts), GetOptions()->GetFile(), ".."); auto Rel = LMakeRelativePath(Opts, OldFolders.Str()); Store->SetAttr(OPT_MailStoreLocation, Rel ? Rel.Get() : OldFolders.Str()); // No need to ask the user for a store name, it'll be // asked later in this method anyway... // Leave the old folder tag in the xml in case the user // downgrades to v1.xx } } } } GetOptions()->Unlock(); if (!MailStores) { if (Callback) Callback(false); return; } } // Set loading flags CmdSend.Enabled(false); CmdReceive.Enabled(false); CmdPreview.Enabled(false); bool Status = LoadMailStores(); if (Tree) { for (auto a: Accounts) { if (!a->Receive.Disabled() && a->Receive.IsPersistant()) a->Receive.Connect(0, false); } } using BoolFn = std::function; auto FinishLoad = new BoolFn ( [this, PrevState, Callback](bool Status) { if (ScribeState == ScribeExiting) { LCloseApp(); } else { d->FoldersLoaded = true; PostEvent(M_SCRIBE_LOADED); } if (ScribeState == ScribeExiting) LCloseApp(); ScribeState = PrevState; if (Callback) Callback(Status); } ); if (Folders.Length() == 0) { auto Dlg = new ScribeFolderDlg(this); Dlg->DoModal([this, Dlg, FinishLoad, Callback, &Status](auto dlg, auto id) { if (id == IDOK) { bool CreateMailStore = false; if (Dlg->Create) { // create folders if (LFileExists(Dlg->FolderFile)) { if (LgiMsg(this, LLoadString(IDS_ERROR_FOLDERS_ALREADY_EXIST), AppName, MB_YESNO) == IDYES) CreateMailStore = true; else LgiMsg(this, LLoadString(IDS_ERROR_WONT_OVERWRITE_FOLDERS), AppName); } else if ((Status = CreateFolders(Dlg->FolderFile))) CreateMailStore = true; } else CreateMailStore = true; if (CreateMailStore) { LXmlTag *MailStores = GetOptions()->LockTag(OPT_MailStores, _FL); if (MailStores) { LXmlTag *Store = MailStores->CreateTag(OPT_MailStore); if (Store) { char p[MAX_PATH_LEN]; LMakePath(p, sizeof(p), GetOptions()->GetFile(), ".."); auto RelPath = LMakeRelativePath(p, Dlg->FolderFile); Store->SetAttr(OPT_MailStoreLocation, RelPath ? RelPath.Get() : Dlg->FolderFile.Get()); } GetOptions()->Unlock(); LoadMailStores(); } } } if (id) (*FinishLoad)(Status); else if (Callback) Callback(false); delete FinishLoad; delete dlg; }); } else { (*FinishLoad)(Status); delete FinishLoad; } } bool ScribeWnd::UnLoadFolders() { if (FolderTasks.Length() > 0 || ScribeState == ScribeLoadingFolders) { // Um, we can't unload folders right now // something is already happening... return false; } AppState PrevState = ScribeState; ScribeState = ScribeUnloadingFolders; OnSelect(); if (MailList) { ScribeFolder *Container = MailList->GetContainer(); if (Container) { // save folder settings Container->SerializeFieldWidths(); } MailList->SetContainer(NULL); MailList->RemoveAll(); } int Error = 0; while (Thing::DirtyThings.Length() > 0) { if (!SaveDirtyObjects()) { Error++; LgiTrace("%s:%i - SaveDirtyObjects failed.\n", _FL); if (Error > 100) { // I think we're stuck... return false; } } } // Unload IMAP folders... for (auto a: Accounts) { if (!a->Receive.Disabled() && a->Receive.IsPersistant()) { a->Receive.Disconnect(); } } if (GetOptions()) { // Unload local folders... LXmlTag *MailStores = GetOptions()->LockTag(OPT_MailStores, _FL); for (size_t i=0; iExpanded(); for (auto ms: MailStores->Children) { char *StoreName = ms->GetAttr(OPT_MailStoreName); if (Folders[i].Name.Equals(StoreName)) { ms->SetAttr(OPT_MailStoreExpanded, Expanded); break; } } } DeleteObj(Folders[i].Root); DeleteObj(Folders[i].Store); } if (MailStores) { GetOptions()->Unlock(); MailStores = NULL; } } Folders.Length(0); d->FoldersLoaded = false; if (ScribeState == ScribeExiting) LCloseApp(); ScribeState = PrevState; return true; } void ScribeWnd::BuildDynMenus() { if (MailMenu) { LString SendMail = LLoadString(IDS_SEND_MAIL); LString ReceiveMail = LLoadString(IDS_RECEIVE_MAIL); LString PreviewMail = LLoadString(IDS_PREVIEW_ON_SERVER); auto ReceiveAll = LLoadString(IDS_RECEIVE_ALL_ACCOUNTS); if (!CmdReceive.MenuItem && ReceiveAll) CmdReceive.MenuItem = MailMenu->AppendItem(ReceiveAll, IDM_RECEIVE_ALL, true); if (!SendMenu && SendMail) { auto s = SendMail.SplitDelimit("\t"); SendMenu = MailMenu->AppendSub(s[0]); } if (!ReceiveMenu && ReceiveMail) { auto s = ReceiveMail.SplitDelimit("\t"); ReceiveMenu = MailMenu->AppendSub(s[0]); } if (!PreviewMenu && PreviewMail) { auto s = PreviewMail.SplitDelimit("\t"); PreviewMenu = MailMenu->AppendSub(s[0]); } } if (!NewTemplateMenu) { auto i = Menu->FindItem(IDM_NO_TEMPLATES); if (i) { NewTemplateMenu = i->GetParent(); } } if (NewTemplateMenu) { NewTemplateMenu->Empty(); int d = 0; ScribeFolder *Templates = GetFolder(FOLDER_TEMPLATES, NULL, true); if (Templates) { Templates->LoadThings(); for (auto i: Templates->Items) { Mail *m = i->IsMail(); if (m) { NewTemplateMenu->AppendItem(m->GetSubject() ? m->GetSubject() : (char*)"(no subject)", IDM_NEW_FROM_TEMPLATE + d, true); d++; } } if (d == 0) { NewTemplateMenu->AppendItem(LLoadString(IDS_NO_ITEMS_IN_FOLDER), -1, false); } } else { NewTemplateMenu->AppendItem(LLoadString(IDS_NO_TEMPLATES), -1, false); } } } int ScribeWnd::GetToolbarHeight() { return (Commands) ? MAX(Commands->Y()-1, 20) : 20; } LToolBar *ScribeWnd::LoadToolbar(LViewI *Parent, const char *File, LAutoPtr &Img) { if (!Img) Img.Reset(LLoadImageList(File)); if (!Img) { LAssert(!"Missing image resource."); return NULL; } LToolBar *Tools = NULL; if (Img) { Tools = new LToolBar; if (Tools) Tools->SetImageList(Img, Img->TileX(), Img->TileY(), false); } else { Tools = LgiLoadToolbar(Parent, File); } if (Tools) { LVariant SizeAdj; LFont *f = Tools->GetFont(); if (f) { if (GetOptions()->GetValue(OPT_UiFontSize, SizeAdj)) { SizeAdj.Cast(GV_INT32); SizeAdj.Value.Int -= 2; f->PointSize(f->PointSize()+SizeAdj.Value.Int); } } Tools->GetCss(true)->BorderSpacing(LCss::Len(LCss::LenPx, SCRIBE_TOOLBAR_BORDER_SPACING_PX)); Tools->TextLabels(ShowToolbarText()); } return Tools; } void ScribeWnd::SetListPane(LView *v) { ThingList *ThingLst = dynamic_cast(v); DynamicHtml *Html = dynamic_cast(v); if (!ThingLst) { DeleteObj(SearchView); if (MailList) MailList->RemoveAll(); } v->Sunken(SUNKEN_CTRL); if ((MailList = ThingLst)) { DeleteObj(TitlePage); if (GetCtrlValue(IDM_ITEM_FILTER)) { OnCommand(IDM_ITEM_FILTER, 0, NULL); } } else { TitlePage = Html; } SetLayout(); } bool ScribeWnd::SetItemPreview(LView *v) { v->Sunken(SUNKEN_CTRL); if (d->SubSplit->IsAttached()) { if (d->LastLayout == 2) { Splitter->SetViewAt(1, v); } else { d->SubSplit->SetViewAt(1, v); } return true; } return false; } ScribeWnd::LayoutMode ScribeWnd::GetEffectiveLayoutMode() { LVariant Mode; GetOptions()->GetValue(OPT_LayoutMode, Mode); ScribeFolder *Cur = GetCurrentFolder(); if (Cur && Cur->IsRoot()) { Mode = FoldersAndList; } if (Mode.CastInt32() == 0) { Mode = FoldersListAndPreview; } return (LayoutMode) Mode.CastInt32(); } void ScribeWnd::SetLayout(LayoutMode Mode) { // int TreeWidth = Tree ? Tree->X() : 200; // int PreviewHt = PreviewPanel ? PreviewPanel->Y() : 200; if (Mode > 0) { LVariant v; GetOptions()->SetValue(OPT_LayoutMode, v = (int)Mode); } Mode = GetEffectiveLayoutMode(); if (!Splitter) return; bool JustPreviewPane = (Mode == FoldersAndList && d->LastLayout == FoldersListAndPreview) || (Mode == FoldersListAndPreview && d->LastLayout == FoldersAndList); LView *Content = NULL; if (TitlePage) Content = TitlePage; else if (MailList) Content = MailList; if (JustPreviewPane) { // Optimized path for hide/show the preview pane that doesn't destroy the tree // control and cause it to lose focus... otherwise we can't set it's focus due // to some weird windows interaction. switch (Mode) { default: case FoldersListAndPreview: { if (Content) Content->Sunken(SUNKEN_CTRL); if (PreviewPanel) PreviewPanel->Sunken(SUNKEN_CTRL); Splitter->SetViewAt(1, d->SubSplit); d->SubSplit->SetVertical(true); int Idx = 0; if (SearchView) d->SubSplit->SetViewAt(Idx++, SearchView); d->SubSplit->SetViewAt(Idx++, Content); d->SubSplit->SetViewAt(Idx++, PreviewPanel); break; } case FoldersAndList: { if (Content) Content->Sunken(SUNKEN_CTRL); if (SearchView) { #if LGI_VIEW_HANDLE if (!d->SubSplit->Handle()) Splitter->SetViewAt(1, d->SubSplit); #endif d->SubSplit->SetVertical(true); Splitter->SetViewAt(1, d->SubSplit); int Idx = 0; if (SearchView) d->SubSplit->SetViewAt(Idx++, SearchView); d->SubSplit->SetViewAt(Idx++, Content); } else { d->SubSplit->Detach(); Splitter->SetViewAt(1, Content); } break; } } } else { if (Tree) Tree->Sunken(SUNKEN_CTRL); if (Content) Content->Sunken(SUNKEN_CTRL); if (PreviewPanel) PreviewPanel->Sunken(SUNKEN_CTRL); switch (Mode) { default: case FoldersListAndPreview: { Splitter->SetVertical(false); d->SubSplit->SetVertical(true); Splitter->SetViewAt(0, Tree); Splitter->SetViewAt(1, d->SubSplit); int Idx = 0; if (SearchView) d->SubSplit->SetViewAt(Idx++, SearchView); d->SubSplit->SetViewAt(Idx++, Content); d->SubSplit->SetViewAt(Idx++, PreviewPanel); DeleteObj(d->SearchSplit); break; } case PreviewOnBottom: { Splitter->SetVertical(true); d->SubSplit->SetVertical(false); Splitter->SetViewAt(0, d->SubSplit); Splitter->SetViewAt(1, PreviewPanel); d->SubSplit->SetViewAt(0, Tree); if (SearchView) { if (!d->SearchSplit) d->SearchSplit = new LBox; d->SubSplit->SetViewAt(1, d->SearchSplit); d->SearchSplit->SetVertical(true); d->SearchSplit->SetViewAt(0, SearchView); d->SearchSplit->SetViewAt(1, Content); } else { d->SubSplit->SetViewAt(1, Content); DeleteObj(d->SearchSplit); } break; } case FoldersAndList: { Splitter->SetVertical(false); Splitter->SetViewAt(0, Tree); if (SearchView) { d->SubSplit->SetVertical(true); Splitter->SetViewAt(1, d->SubSplit); d->SubSplit->SetViewAt(0, SearchView); d->SubSplit->SetViewAt(1, Content); } else { d->SubSplit->Detach(); Splitter->SetViewAt(1, Content); } DeleteObj(d->SearchSplit); break; } case ThreeColumn: { Splitter->SetVertical(false); Splitter->SetViewAt(0, Tree); if (SearchView) { d->SubSplit->SetVertical(true); Splitter->SetViewAt(1, d->SubSplit); d->SubSplit->SetViewAt(0, SearchView); d->SubSplit->SetViewAt(1, Content); } else { d->SubSplit->Detach(); Splitter->SetViewAt(1, Content); } Splitter->SetViewAt(2, PreviewPanel); DeleteObj(d->SearchSplit); break; } } } if (!SearchView) { LVariant Pos = 200; GetOptions()->GetValue(OPT_SplitterPos, Pos); if (Pos.CastInt32() < 10) Pos = 200; Splitter->Value(Pos.CastInt32()); LRect r = Splitter->GetPos(); r.x2++; Splitter->SetPos(r); if (d->SubSplit->IsAttached()) { Pos = 200; GetOptions()->GetValue(OPT_SubSplitPos, Pos); if (Pos.CastInt32() < 10) Pos = 200; d->SubSplit->Value(Pos.CastInt32()); } } PourAll(); #ifdef LINUX LYield(); #endif d->LastLayout = Mode; } void ScribeWnd::SetupUi() { // Show the window if (!SerializeState(GetOptions(), OPT_ScribeWndPos, true)) { LRect r(0, 0, 1023, 767); SetPos(r); MoveToCenter(); } Visible(true); // Main toolbar Commands = LoadToolbar(this, GetResourceFile(ResToolbarFile), ToolbarImgs); if (Commands) { Commands->Attach(this); #ifdef MAC Commands->Raised(false); #else Commands->Raised(RAISED_LOOK); #endif Commands->AppendButton(RemoveAmp(LLoadString(IDS_NEW_EMAIL)), IDM_NEW_EMAIL, TBT_PUSH, true, IMG_NEW_MAIL); Commands->AppendButton(RemoveAmp(LLoadString(IDS_NEW_CONTACT)), IDM_NEW_CONTACT, TBT_PUSH, true, IMG_NEW_CONTACT); Commands->AppendSeparator(); CmdSend.ToolButton = Commands->AppendButton(RemoveAmp(LLoadString(IDS_SEND)), IDM_SEND_MAIL, TBT_PUSH, true, IMG_SEND); Commands->AppendButton("+", IDM_RECEIVE_AND_SEND, TBT_PUSH, true, -2); CmdReceive.ToolButton = Commands->AppendButton(RemoveAmp(LLoadString(IDS_RECEIVE)), IDM_RECEIVE_MAIL, TBT_PUSH, true, IMG_RECEIVE); CmdPreview.ToolButton = Commands->AppendButton(RemoveAmp(LLoadString(IDS_PREVIEW)), IDM_PREVIEW_POP3, TBT_PUSH, true, IMG_PREVIEW); Commands->AppendSeparator(); Commands->AppendButton(RemoveAmp(LLoadString(IDS_DELETE)), IDM_DELETE, TBT_PUSH, true, IMG_TRASH); Commands->AppendButton(RemoveAmp(LLoadString(IDS_SPAM)), IDM_DELETE_AS_SPAM, TBT_PUSH, true, IMG_DELETE_SPAM); Commands->AppendSeparator(); Commands->AppendButton(RemoveAmp(LLoadString(IDS_REPLY)), IDM_REPLY, TBT_PUSH, true, IMG_REPLY); Commands->AppendButton(RemoveAmp(LLoadString(IDS_REPLYALL)), IDM_REPLY_ALL, TBT_PUSH, true, IMG_REPLY_ALL); Commands->AppendButton(RemoveAmp(LLoadString(IDS_FORWARD)), IDM_FORWARD, TBT_PUSH, true, IMG_FORWARD); Commands->AppendButton(RemoveAmp(LLoadString(IDS_BOUNCE)), IDM_BOUNCE, TBT_PUSH, true, IMG_BOUNCE); Commands->AppendSeparator(); Commands->AppendButton(RemoveAmp(LLoadString(IDS_PRINT)), IDM_PRINT, TBT_PUSH, true, IMG_PRINT); Commands->AppendButton(RemoveAmp(LLoadString(IDS_CALENDAR)), IDM_CALENDAR, TBT_PUSH, true, IMG_CALENDAR); Commands->AppendButton(RemoveAmp(LLoadString(IDS_ITEM_FILTER)), IDM_ITEM_FILTER, TBT_TOGGLE, true, IMG_SEARCH); Commands->AppendButton(RemoveAmp(LLoadString(IDS_THREAD)), IDM_THREAD, TBT_TOGGLE, true, IMG_THREADS); d->ShowConsoleBtn = Commands->AppendButton(RemoveAmp(LLoadString(IDS_SHOW_CONSOLE)), IDM_SHOW_CONSOLE, TBT_PUSH, true, IMG_CONSOLE_NOMSG); Commands->AppendSeparator(); Commands->AppendButton(RemoveAmp(LLoadString(IDS_HELP)), IDM_HELP, TBT_PUSH, true, IMG_HELP); Commands->Customizable(GetOptions(), OPT_ScribeWndToolbar); if (d->ScriptToolbar.Reset(new LScriptUi(Commands))) d->ScriptToolbar->SetupCallbacks(this, 0, 0, LApplicationToolbar); PourAll(); } CmdSend.Enabled(false); CmdReceive.Enabled(false); // Preview and status windows PreviewPanel = new LPreviewPanel(this); StatusPanel = new AccountStatusPanel(this, ImageList); if (PreviewPanel && StatusPanel) { #ifdef MAC StatusPanel->Raised(false); #else StatusPanel->Raised(RAISED_LOOK); #endif StatusPanel->Attach(this); PourAll(); } // Splitter window, for folders and item list Tree = new MailTree(this); if (ImageList) { Tree->AskImage(true); Tree->SetImageList(ImageList, false); } d->SubSplit = new LBox; Splitter = new LBox; if (Splitter) { Splitter->Attach(this); #ifdef MAC Splitter->Raised(false); #else Splitter->Raised(RAISED_LOOK); #endif SetLayout(); } #if WINNATIVE TrayIcon.Load(MAKEINTRESOURCE(IDI_SMALL)); TrayIcon.Load(MAKEINTRESOURCE(IDI_ERR)); TrayIcon.Load(MAKEINTRESOURCE(IDI_MAIL)); TrayIcon.Load(MAKEINTRESOURCE(IDI_BLANK)); #else TrayIcon.Load(_T("tray_small.png")); TrayIcon.Load(_T("tray_error.png")); TrayIcon.Load(_T("tray_mail.png")); #endif LStringPipe s(256); LVariant UserName; s.Print("%s", AppName); if (GetOptions()->GetValue(OPT_UserName, UserName)) { s.Print(" [%s]", UserName.Str()); } auto AppTitle = s.NewLStr(); TrayIcon.Name(AppTitle); Name(AppTitle); TrayIcon.Value(TRAY_ICON_NORMAL); TrayIcon.Visible(true); auto Item = Menu->FindItem(IDM_SCRIPTING_CONSOLE); if (Item) { LVariant v; if (GetOptions()->GetValue(OPT_ShowScriptConsole, v)) { Item->Checked(v.CastInt32() != 0); LScribeScript::Inst->ShowScriptingWindow(Item->Checked()); } } if (Tree) { Tree->Focus(true); } } Thing *ScribeWnd::CreateItem(int Type, ScribeFolder *Folder, bool Ui) { auto FolderStore = Folder && Folder->GetObject() ? Folder->GetObject()->GetStore() : NULL; auto DefaultStore = GetDefaultMailStore(); auto Store = FolderStore ? FolderStore : (DefaultStore ? DefaultStore->Store : NULL); if (!Store) { LAssert(!"no store"); LgiTrace("%s:%i - No store for creating calendar object.\n", _FL); return NULL; } auto Obj = Store->Create(Type); if (!Obj) { LAssert(!"create failed"); LgiTrace("%s:%i - store failed to create object.\n", _FL); return NULL; } #define HANDLE_CREATE_ITEM(Magic, Type) \ case Magic: \ { \ auto o = new Type(this, Obj); \ if (!o) \ { \ LgiTrace("%s:%i - Alloc failed.\n", _FL); \ break; \ } \ if (Folder) o->SetParentFolder(Folder); \ if (Ui) o->DoUI(); \ return o; \ } switch ((uint32_t)Type) { case MAGIC_MAIL: { // create a new mail message Mail *m = new Mail(this, Obj); if (!m) { LgiTrace("%s:%i - Alloc failed.\n", _FL); break; } if (!m->GetObject()) { m->DecRef(); return 0; } m->OnCreate(); if (Folder) m->SetParentFolder(Folder); if (Ui) m->DoUI(); return m; } HANDLE_CREATE_ITEM(MAGIC_CONTACT, Contact) HANDLE_CREATE_ITEM(MAGIC_CALENDAR, Calendar) HANDLE_CREATE_ITEM(MAGIC_FILTER, Filter) HANDLE_CREATE_ITEM(MAGIC_GROUP, ContactGroup) default: LAssert(!"Unhandled object type."); break; } return NULL; } void ScribeWnd::OnPaint(LSurface *pDC) { #if 0 pDC->Colour(LColour::Red); pDC->Rectangle(); #else LCssTools Tools(this); auto c = GetClient(); // c.Offset(0, -26); Tools.PaintContent(pDC, c); #endif } int CompareContacts(Contact **a, Contact **b) { if (a && b) { auto A = (*a)->GetFirst(); auto B = (*b)->GetFirst(); if (A && B) return _stricmp(A, B); } return 0; } bool ScribeWnd::OpenAMail(ScribeFolder *Folder) { if (Folder && Tree) { Folder->LoadThings(); for (auto i: Folder->Items) { Mail *m = i->IsMail(); if (m && !(m->GetFlags() & MAIL_READ)) { Tree->Select(Folder); m->DoUI(); return true; } } for (auto *t=Folder->GetChild(); t; t=t->GetNext()) { ScribeFolder *f = dynamic_cast(t); if (OpenAMail(f)) { return true; } } } return false; } void AddContactToMenu(LSubMenu *Menu, Contact *c, ssize_t Index) { if (!c || Index < 0) return; auto Email = c->GetEmail(); if (!Email) return; // has an email, list it auto First = c->GetFirst(); auto Last = c->GetLast(); if (First || Last) { char Buf[256]; sprintf_s(Buf, sizeof(Buf), "%s %s", (First)?First:"", (Last)?Last:""); auto Item = Menu->AppendItem(Buf, TRAY_CONTACT_BASE + (int)Index, true); if (Item) Item->Icon(ICON_CONTACT); } } void ScribeWnd::AddContactsToMenu(LSubMenu *Menu) { if (!Menu) return; d->TrayMenuContacts.Sort(CompareContacts); if (((ssize_t)d->TrayMenuContacts.Length() << 4) > GdcD->Y() - 200) { // Group contacts by starting letter LArray Alpha[26]; LArray Other; for (auto c: d->TrayMenuContacts) { auto First = c->GetFirst(); auto Last = c->GetLast(); auto Email = c->GetEmail(); if (Email) { // has an email, list it if (First || Last) { auto Name = First?First:Last; if ( (*Name >= 'a' && *Name <= 'z') || (*Name >= 'A' && *Name <= 'Z') ) { int Ind = tolower(*Name) - 'a'; if (Ind >= 0 && Ind < CountOf(Alpha)) Alpha[Ind].Add(c); else Other.Add(c); } else Other.Add(c); } } } for (int i=0; i 0) { char Group[64]; sprintf_s(Group, sizeof(Group), "%c...", 'a' + i); auto Sub = Menu->AppendSub(Group); if (Sub) { for (auto c: Alpha[i]) AddContactToMenu(Sub, c, d->TrayMenuContacts.IndexOf(c)); } } } if (Other.Length()) { auto Sub = Menu->AppendSub("Other..."); if (Sub) { for (auto c: Other) AddContactToMenu(Sub, c, d->TrayMenuContacts.IndexOf(c)); } } } else { // Display all... for (size_t i=0; iTrayMenuContacts.Length(); i++) { AddContactToMenu(Menu, d->TrayMenuContacts[i], i); } } } void ScribeWnd::OnUrl(const char *Url) { LUri u(Url); if (u.sProtocol && !_stricmp(u.sProtocol, "mailto")) { CreateMail(0, Url, 0); } } void ScribeWnd::OnReceiveFiles(LArray &Files) { int UnknownFormats = 0; int64 Period = LCurrentTime() - LastDrop; if (Period > 500) // Lock out drops within 500ms of an LGI drop { LString sSend, sPages; bool HasSend = LAppInst->GetOption("send", sSend); bool HasPrint = LAppInst->GetOption("p", sPages); LArray NewMail; for (unsigned i=0; i str(new LFile); if (str->Open(f, O_READ)) { if (t->Import(t->AutoCast(str), MimeType)) { if (HasSend) { Mail *m = t->IsMail(); if (m) { NewMail.Add(m); m->SetFlags(m->GetFlags() | MAIL_CREATED | MAIL_READ); } } else { t->DoUI(); } } } } } else UnknownFormats++; } bool SendNow = sSend ? atoi(sSend) != 0 : false; for (unsigned i=0; iSetFlags(m->GetFlags() | MAIL_READY_TO_SEND); m->Save(); } else if (HasPrint) { ThingPrint(NULL, m, GetPrinter(), 0, sPages ? atoi(sPages) : 0); } else { m->DoUI(); } } if (SendNow) Send(); } } void ScribeWnd::OnTrayClick(LMouse &m) { if (m.Down()) { #ifndef MAC // No support for different mouse button info so the default // action is to show the sub-menu of contacts and actions. if (m.IsContextMenu()) #endif { LWindow::OnTrayClick(m); } #ifndef MAC else if (m.Left()) { if (m.Double()) { if (Mail::NewMailLst.Length() > 0) { ScribeFolder *InBox = GetFolder(FOLDER_INBOX); OpenAMail(InBox); } } else { if (GetZoom() == LZoomMin) { SetZoom(LZoomNormal); } else { if (Obscured()) SetZoom(LZoomNormal); // Bounce in front, first. else SetZoom(LZoomMin); } Visible(true); MoveOnScreen(); Raise(); if (MailList) { MailList->Focus(true); } } } else if (m.Middle()) { Mail::NewMailLst.Empty(); } #endif } } void ScribeWnd::OnTrayMenu(LSubMenu &m) { m.SetImageList(ImageList, false); d->TrayMenuContacts.Length(0); #ifdef MAC m.AppendItem(LLoadString(IDS_OPEN), IDM_OPEN); m.AppendSeparator(); #endif LHashTbl,Contact*> Added; LArray Srcs = GetThingSources(MAGIC_CONTACT); for (auto c: Srcs) { c->LoadThings(); for (auto i: c->Items) { Contact *c = i->IsContact(); if (!c) continue; bool IsAdded = false; auto Emails = c->GetEmails(); for (auto e: Emails) { if (Added.Find(e)) IsAdded = true; } if (Emails.Length() && !IsAdded) { for (auto e: Emails) Added.Add(e, c); d->TrayMenuContacts.Add(c); } } } AddContactsToMenu(&m); m.AppendSeparator(); if (Mail::NewMailLst.Length() > 0) { int i=0; for (auto ml: Mail::NewMailLst) { LStringPipe p; // This code figures out how many UTF characters to print. We // can't split a UTF character because downstream conversions // will fail. const char *Subj = ml->GetSubject(); if (!Subj) Subj = "(No Subject)"; LUtf8Ptr u((uint8_t*)Subj); while ((int32)u && u.GetPtr() - (uchar*)Subj < 64) u++; ssize_t Bytes = u.GetPtr() - (uchar*)Subj; p.Print("%.*s, %s <%s>", Bytes, Subj?Subj:(char*)"(No Subject)", ml->GetFromStr(FIELD_NAME), ml->GetFromStr(FIELD_EMAIL)); LAutoString a(p.NewStr()); LAssert(LIsUtf8(a)); auto Item = m.AppendItem(a, TRAY_MAIL_BASE+i++, true); if (Item) { Item->Icon(ICON_UNREAD_MAIL); } } m.AppendSeparator(); } if (GetZoom() == LZoomMin) { m.AppendItem(LLoadString(IDS_OPEN), IDM_OPEN, true); } auto NewMail = m.AppendItem(LLoadString(IDS_NEW_EMAIL), IDM_NEW_EMAIL); if (NewMail) NewMail->Icon(ICON_UNSENT_MAIL); m.AppendItem(LLoadString(IDS_EXIT), IDM_EXIT); } void ScribeWnd::OnTrayMenuResult(int MenuId) { switch (MenuId) { case IDM_OPEN: { if (GetZoom() == LZoomMin) { SetZoom(LZoomNormal); } Visible(true); Raise(); if (MailList) { MailList->Focus(true); } break; } case IDM_NEW_EMAIL: { CreateMail(); break; } case IDM_EXIT: { d->IngoreOnClose = true; PostEvent(M_CLOSE); break; } default: { auto i = MenuId - TRAY_CONTACT_BASE; Contact *c = d->TrayMenuContacts.IdxCheck(i) ? d->TrayMenuContacts[i] : NULL; if (c) { CreateMail(c); } Mail *m = Mail::NewMailLst[MenuId - TRAY_MAIL_BASE]; if (m) { Mail::NewMailLst.Delete(m); m->DoUI(); } break; } } } void ScribeWnd::OnZoom(LWindowZoom Action) { if (Action == LZoomMin) { LVariant i; if (GetOptions()->GetValue(OPT_MinimizeToTray, i) && i.CastInt32()) { Visible(false); } } } struct UserInput { std::function Callback; LView *Parent; LString Msg; bool Password; UserInput() { Password = false; } }; void ScribeWnd::GetUserInput(LView *Parent, LString Msg, bool Password, std::function Callback) { if (InThread()) { auto Inp = new LInput(Parent ? Parent : this, "", Msg, AppName, Password); Inp->DoModal([this, Inp, Callback](auto dlg, auto id) { if (Callback) Callback(id ? Inp->GetStr() : NULL); delete dlg; }); } auto i = new UserInput; i->Parent = Parent; i->Msg = Msg; i->Password = Password; i->Callback = Callback; if (!PostEvent(M_GET_USER_INPUT, (LMessage::Param)i)) { LAssert(!"PostEvent failed."); if (Callback) Callback(NULL); } } LMessage::Result ScribeWnd::OnEvent(LMessage *Msg) { TrayIcon.OnEvent(Msg); BayesianFilter::OnEvent(Msg); switch (Msg->Msg()) { case M_UNIT_TEST: { LAutoPtr j((LJson*)Msg->A()); if (!j) break; auto cmd = j->Get("cmd"); if (!cmd) break; if (cmd.Equals("somecmd")) { } break; } case M_CALENDAR_SOURCE_EVENT: { CalendarSource *cs = (CalendarSource*)Msg->A(); LAutoPtr m((LMessage*)Msg->B()); if (cs && m) cs->OnEvent(m); break; } case M_GET_USER_INPUT: { LAutoPtr i((UserInput*)Msg->A()); LAssert(i); LAssert(InThread()); // Least we get stuck in an infinite loop GetUserInput(i->Parent, i->Msg, i->Password, i->Callback); break; } case M_SET_HTML: { LAutoPtr Html((LString*)Msg->A()); if (PreviewPanel && Html) { LVariant Ret; LArray Arg; Arg.Add(new LVariant(Html->Get())); PreviewPanel->CallMethod(DomToStr(SdSetHtml), &Ret, Arg); Arg.DeleteObjects(); } break; } case M_NEW_CONSOLE_MSG: { if (d->ShowConsoleBtn) d->ShowConsoleBtn->Image(IDM_CONSOLE_MSGS); break; } case M_NEEDS_CAP: { LAutoString c((char*)Msg->A()); LAutoString param((char*)Msg->B()); NeedsCapability(c, param); return 0; break; } case M_STORAGE_EVENT: { LDataStoreI *Store = LDataStoreI::Map.Find((int)Msg->A()); if (Store) Store->OnEvent((void*)Msg->B()); break; } case M_SCRIBE_SET_MSG_FLAG: { LAutoString p((char*)Msg->A()); if (p) { char *d = strrchr(p, '/'); if (d) { *d++ = 0; ScribeFolder *f = GetFolder(p); if (f) { LUri u; LString a = u.DecodeStr(d); f->GetMessageById(a, [&](auto r) { if (r) { int ExistingFlags = r->GetFlags(); int NewFlag = (int)Msg->B(); r->SetFlags(ExistingFlags | NewFlag); } }); } } } break; } case M_SCRIBE_DEL_THING: { Thing *t = (Thing*)Msg->A(); DeleteObj(t); break; } case M_SCRIBE_LOADED: { if (d->FoldersLoaded) { // Ok let the user in CmdSend.Enabled(true); CmdReceive.Enabled(true); CmdPreview.Enabled(true); } break; } case M_SCRIBE_THREAD_DONE: { // Finialize connection AccountThread *Thread = (AccountThread*) Msg->A(); Accountlet *Acc = (Accountlet*) Msg->B(); if (Thread && Acc) { OnAfterConnect(Acc->GetAccount(), Acc->IsReceive()); d->NewMailTimeout = 2; Acc->OnThreadDone(); } else { LAssert(0); } break; } case M_SCRIBE_MSG: { char *m = (char*)Msg->A(); if (m) { if (Msg->B()) { if (LgiMsg(this, m, AppName, MB_YESNO) == IDYES) { PostEvent(M_COMMAND, IDM_OPTIONS, 0); } } else { LgiMsg(this, "%s", AppName, MB_OK, m); } DeleteArray(m); } break; } /* case M_SCRIBE_LOG_MSG: { List *Log = (List*)Msg->A(); LAutoPtr Entry((LogEntry*)Msg->B()); if (ScribeState != ScribeExiting && Log && Entry) { Log->Insert(Entry.Release()); // Trim long list... while (Log->Length() > 1024) { LogEntry *e = Log->First(); Log->Delete(e); DeleteObj(e); } } break; } */ case M_SCRIBE_NEW_MAIL: { if (Lock(_FL)) { if (NewMailDlg) { NewMailDlg->AddThings(&Mail::NewMailLst); } Unlock(); } break; } case M_SCRIBE_OPEN_THING: { Thing *t = (Thing*) Msg->A(); if (t) { t->DoUI(); } break; } case M_SCRIBE_ITEM_SELECT: { if (!MailList || !IsAttached()) break; List Things; MailList->GetSelection(Things); OnSelect(&Things); break; } case M_URL: { LAutoPtr Url((LString*)Msg->A()); if (Url) { LUri u(*Url); if (u.sProtocol && !_stricmp(u.sProtocol, "mailto")) { Mailto mt(this, *Url); if (mt.To.Length() > 0) { Thing *t = CreateItem(MAGIC_MAIL, NULL, false); if (t) { Mail *m = t->IsMail(); if (m) { mt.Apply(m); m->DoUI(); } else DeleteObj(t); } } } } break; } } return LWindow::OnEvent(Msg); } bool ScribeWnd::IsMyEmail(const char *Email) { if (Email) { LVariant e; for (auto a : *GetAccounts()) { LVariant e = a->Identity.Email(); if (e.Str() && _stricmp(Email, e.Str()) == 0) { return true; } } } return false; } int ScribeWnd::GetMaxPages() { return d->PrintMaxPages; } void ScribeWnd::ThingPrint(std::function Callback, ThingType *m, LPrinter *Printer, LView *Parent, int MaxPages) { d->PrintMaxPages = MaxPages; if (!Printer) Printer = GetPrinter(); if (!Printer) { if (Callback) Callback(false); return; } Thing *t = dynamic_cast(m); if (!t) { if (Callback) Callback(false); return; } ScribePrintContext Events(this, t); Printer->Print( &Events, [&](auto pages) { if (pages == Events.OnBeginPrintError) { LgiMsg(Parent, "Printing failed: %s", AppName, MB_OK, Printer->GetErrorMsg().Get()); if (Callback) Callback(false); } else if (Callback) { Callback(true); } }, AppName, -1, Parent ? Parent : this); } bool ScribeWnd::MailReplyTo(Mail *m, bool All) { bool Status = false; if (m) { LDataStoreI *Store = m->GetObject() ? m->GetObject()->GetStore() : NULL; LDataI *NewMailObj = Store && Store->GetInt(FIELD_STORE_TYPE) == Store3Sqlite ? Store->Create(MAGIC_MAIL) : NULL; Mail *NewMail = new Mail(this, NewMailObj); if (NewMail) { if (NewMail->GetObject()) { NewMail->OnReply(m, All, true); LView *w = NewMail->DoUI(); if (w) { LViewI *t = w->FindControl(IDC_TEXT); if (t) { t->Focus(true); } } Status = true; } else DeleteObj(NewMail); } } return Status; } bool ScribeWnd::MailForward(Mail *m) { bool Status = false; if (m) { Mail *NewMail = new Mail(this); if (NewMail) { if (NewMail->OnForward(m, true)) { NewMail->DoUI(); Status = true; } else { NewMail->DecRef(); } } } return Status; } bool ScribeWnd::MailBounce(Mail *m) { bool Status = false; if (m) { Mail *NewMail = new Mail(this); if (NewMail) { if (NewMail->OnBounce(m, true)) { NewMail->DoUI(); Status = true; } else { DeleteObj(NewMail); } } } return Status; } Mail *ScribeWnd::CreateMail(Contact *c, const char *Email, const char *Name) { Mail *m = dynamic_cast(CreateItem(MAGIC_MAIL, NULL, false)); if (m) { bool IsMailTo = false; if (Email) { IsMailTo = _strnicmp(Email, "mailto:", 7) == 0; if (IsMailTo) { Mailto mt(this, Email); mt.Apply(m); } } MailUi *UI = dynamic_cast(m->DoUI()); if (UI) { if (c) { UI->AddRecipient(c); } if (Email && !IsMailTo) { UI->AddRecipient(Email, Name); } } } return m; } Mail *ScribeWnd::LookupMailRef(const char *MsgRef, bool TraceAllUids) { if (!MsgRef) return 0; LAutoString p(NewStr(MsgRef)); char *RawUid = strrchr(p, '/'); if (RawUid) { *RawUid++ = 0; LUri u; LString Uid = u.DecodeStr(RawUid); // Try the mail message map first... Mail *m = Mail::GetMailFromId(Uid); if (m) return m; // Ok, not found, so look in last known folder... ScribeFolder *f = GetFolder(p); if (f) { for (auto t : f->Items) { Mail *m = t->IsMail(); if (m) { auto s = m->GetMessageId(); if (s && !strcmp(s, Uid)) { return m; } if (TraceAllUids) LgiTrace("\t%s\n", s); } } } } return 0; } void ScribeWnd::OnBayesAnalyse(const char *Msg, const char *WhiteListEmail) { LString s, q; s.Printf("
%s
", Msg); if (WhiteListEmail) { q.Printf(LLoadString(IDS_REMOVE_WHITELIST), WhiteListEmail); s += LString("
") + q; } s += ""; LHtmlMsg([&](auto result) { if (result == IDYES) RemoveFromWhitelist(WhiteListEmail); }, this, s, AppName, WhiteListEmail ? MB_YESNO : MB_OK); } bool ScribeWnd::OnBayesResult(const char *MailRef, double Rating) { Mail *m = LookupMailRef(MailRef); if (m) return OnBayesResult(m, Rating); #ifdef _DEBUG else { LgiTrace("%s:%i - error finding mail ref: %s\n", _FL, MailRef); LookupMailRef(MailRef, true); LAssert(!"We should always be able to resolve the reference, unless m is completely deleted"); } #endif return false; } bool ScribeWnd::OnBayesResult(Mail *m, double Rating) { if (!m) return false; LVariant v; GetOptions()->GetValue(OPT_BayesThreshold, v); double BayesThresh = v.CastDouble(); if (BayesThresh < 0.1) BayesThresh = 0.1; if (BayesThresh > 1.0) BayesThresh = 1.0; if (Rating < BayesThresh) { // Not spam, so we continue new mail processing if (m->NewEmail == Mail::NewEmailBayes) { List Nm; Nm.Insert(m); m->NewEmail = Mail::NewEmailGrowl; OnNewMail(&Nm, true); } return false; } // Spam is pink! m->SetMarkColour(Rgb32(255, 0, 0)); m->SetFlags(m->GetFlags() | MAIL_BAYES_SPAM); ScribeBayesianFilterMode FilterMode = BayesOff; if (GetOptions()->GetValue(OPT_BayesFilterMode, v)) FilterMode = (ScribeBayesianFilterMode)v.CastInt32(); if (FilterMode == BayesTrain) { // Move to folder LVariant MoveToPath; if (!GetOptions()->GetValue(OPT_BayesMoveTo, MoveToPath)) { MoveToPath = "/Spam/Probably"; } ScribeFolder *f = GetFolder(MoveToPath.Str()); if (f) { LArray Items; Items.Add(m); f->MoveTo(Items); List obj; obj.Insert(m); OnNewMail(&obj, false); } } else { m->DeleteAsSpam(this); } return true; } static int AccountCmp(ScribeAccount *a, ScribeAccount *b, int Data) { return a->Identity.Sort() - b->Identity.Sort(); } class ScribePasteState : public LProgressDlg { ScribeWnd *App = NULL; ScribeFolder *Folder = NULL; LAutoPtr Data; ssize_t Size = 0; LDataStoreI::StoreTrans Trans; LProgressPane *LoadPane = NULL, *SavePane = NULL; ScribeClipboardFmt *tl = NULL; uint32_t Errors = 0; ssize_t Idx = 0; enum PasteState { LoadingThings, SavingThings, } State = LoadingThings; public: ScribePasteState(ScribeWnd *app, ScribeFolder *folder, LAutoPtr data, ssize_t size) : LProgressDlg(app), App(app), Folder(folder), Data(data), Size(size) { // Paste 'ScribeThingList' tl = (ScribeClipboardFmt*)Data.Get(); Trans = Folder->GetObject()->GetStore()->StartTransaction(); LoadPane = ItemAt(0); LoadPane->SetDescription("Loading objects..."); LoadPane->SetRange(tl->Length()); SavePane = Push(); SavePane->SetRange(tl->Length()); SavePane->SetDescription("Saving: No errors..."); // LProgressDlg will do a SetPulse in it's OnCreate } void OnPulse() { auto Start = LCurrentTime(); static int TimeSlice = 300; //ms if (State == LoadingThings) { while ( Idx < tl->Length() && !IsCancelled() && LCurrentTime() - Start < TimeSlice) { Thing *t = tl->ThingAt(Idx++); if (!t) continue; auto Obj = t->GetObject(); if (Obj->GetInt(FIELD_LOADED) < Store3Loaded) Obj->SetInt(FIELD_LOADED, Store3Loaded); } Value(Idx); if (Idx >= tl->Length()) { State = SavingThings; Idx = 0; } } else if (State == SavingThings) { while ( Idx < tl->Length() && !IsCancelled() && LCurrentTime() - Start < TimeSlice) { Thing *t = tl->ThingAt(Idx++); if (!t) continue; auto Obj = t->GetObject(); LAssert(Obj->GetInt(FIELD_LOADED) == Store3Loaded); // Load loop should have done this already Thing *Dst = App->CreateItem(Obj->Type(), Folder, false); if (Dst) { *Dst = *t; Dst->Update(); if (!Dst->Save(Folder)) { LString s; s.Printf("Saving: " LPrintfSSizeT " error(s)", ++Errors); SetDescription(s); } } else Errors++; } SavePane->Value(Idx); if (Idx >= tl->Length()) { if (Errors > 0) LgiMsg(this, "Failed to save %i of %i objects.", AppName, MB_OK, Errors, tl->Length()); Quit(); return; } } LProgressDlg::OnPulse(); } }; int ScribeWnd::OnCommand(int Cmd, int Event, OsView WndHandle) { // Send mail multi-menu if (Cmd >= IDM_SEND_FROM && Cmd <= IDM_SEND_FROM + (ssize_t)Accounts.Length()) { Send(Cmd - IDM_SEND_FROM); return 0; } // Receive mail multi-menu if (Cmd >= IDM_RECEIVE_FROM && Cmd < IDM_RECEIVE_FROM + (ssize_t)Accounts.Length()) { Receive(Cmd - IDM_RECEIVE_FROM); return 0; } // Preview mail multi-menu if (Cmd >= IDM_PREVIEW_FROM && Cmd < IDM_PREVIEW_FROM + (ssize_t)Accounts.Length()) { Preview(Cmd - IDM_PREVIEW_FROM); return 0; } // Identity multi-menu if (Cmd >= IDM_IDENTITY_BASE && Cmd <= IDM_IDENTITY_BASE + (ssize_t)Accounts.Length()) { SetCurrentIdentity(Cmd - IDM_IDENTITY_BASE - 1); return 0; } // Is this a script tool? if (LScribeScript::Inst && Cmd >= IDM_TOOL_SCRIPT_BASE && Cmd < IDM_TOOL_SCRIPT_BASE + (int)d->Scripts.Length()) { // Do tools menu callback... find the right callback.... LArray c; if (GetScriptCallbacks(LToolsMenu, c)) { for (unsigned i=0; iFunc && c[i]->Param == Cmd) { // Call the callback char Msg[MAX_PATH_LEN]; LScribeScript::Inst->GetLog()->Write ( Msg, sprintf_s(Msg, sizeof(Msg), "\n\nRunning tool script '%s'...\n", c[i]->Script->Code->GetFileName()) ); // Setup the arguments... LScriptArguments Args(NULL); Args.New() = new LVariant((LDom*)this); Args.New() = new LVariant(Cmd); // Call the method ExecuteScriptCallback(*c[i], Args); // Cleanup Args.DeleteObjects(); break; } } } return 0; } // New from template multi-menu if (Cmd >= IDM_NEW_FROM_TEMPLATE && Cmd < IDM_NEW_FROM_TEMPLATE + 100) { int Index = Cmd - IDM_NEW_FROM_TEMPLATE; ScribeFolder *Templates = GetFolder(FOLDER_TEMPLATES); if (Templates) { Templates->LoadThings(); for (auto i: Templates->Items) { Mail *m = i->IsMail(); if (m) { if (Index == 0) { Thing *t = CreateItem(MAGIC_MAIL, 0, false); // GetFolder(FOLDER_OUTBOX) Mail *NewMail = IsMail(t); if (NewMail) { *NewMail = (Thing&)*m; NewMail->DoUI(); break; } } Index--; } } } return 0; } switch (Cmd) { // File menu case IDM_MANAGE_MAIL_STORES: { auto Dlg = new ManageMailStores(this); Dlg->DoModal([this, Dlg](auto dlg, auto id) { LAutoPtr mem(dlg); if (id) { SaveOptions(); if (!UnLoadFolders()) return; LXmlTag *Ms = GetOptions()->LockTag(OPT_MailStores, _FL); if (Ms) { while (Ms->Children.Length()) delete Ms->Children[0]; LXmlTag *t = Dlg->Options.GetChildTag(OPT_MailStores); if (t) { for (auto c: t->Children) { LXmlTag *n = new LXmlTag; n->Copy(*c, true); Ms->InsertTag(n); } } GetOptions()->Unlock(); } LVariant v; GetOptions()->SetValue(OPT_CreateFoldersIfMissing, v = true); if (!Dlg->Options.GetValue(OPT_StartInFolder, v)) v.Empty(); GetOptions()->SetValue(OPT_StartInFolder, v); LoadFolders(NULL); } }); break; } case IDM_REPLICATE: { auto Dlg = new ReplicateDlg(this); Dlg->DoModal([this, Dlg](auto dlg, auto id) { if (id) { UnLoadFolders(); Dlg->StartProcess(); // Don't delete dialog... let it run } else delete dlg; }); break; } case IDM_SECURITY: { // Check for user perm password... // No point allow any old one to edit the security settings. auto ShowDialog = [&]() { auto Dlg = new SecurityDlg(this); Dlg->DoModal(NULL); }; GPassword p; if (p.Serialize(GetOptions(), OPT_UserPermPassword, false)) { GetAccessLevel(this, PermRequireUser, "Security Settings", [&](bool Allow) { if (Allow) ShowDialog(); }); } else { ShowDialog(); } break; } case IDM_OPTIONS: { LVariant ShowTotals; GetOptions()->GetValue(OPT_ShowFolderTotals, ShowTotals); // do the dialog auto Dlg = new OptionsDlg(this); Dlg->DoModal([this, Dlg, ShowTotals](auto dlg, auto id) { if (id) { // set up the POP3 accounts SetupAccounts(); SaveOptions(); // close any IMAP accounts that are now disabled. for (auto a : Accounts) { if (a->Receive.IsConfigured() && a->Receive.IsPersistant()) { if (a->Receive.Disabled()) a->Receive.Disconnect(); else Receive(a->GetIndex()); } } // List/Tree view options update LVariant i; if (GetOptions()->GetValue(OPT_ShowFolderTotals, i) && i.CastInt32() != ShowTotals.CastInt32()) { Tree->UpdateAllItems(); } if (GetOptions()->GetValue(OPT_PreviewLines, i)) { Mail::PreviewLines = i.CastInt32() != 0; } if (MailList) { if (GetOptions()->GetValue(OPT_GridLines, i)) { MailList->DrawGridLines(i.CastInt32() != 0); } MailList->Invalidate(); } // date formats if (GetOptions()->GetValue(OPT_DateFormat, i)) { int Idx = i.CastInt32(); if (Idx >= 0 && Idx < CountOf(DateTimeFormats)) { LDateTime::SetDefaultFormat(DateTimeFormats[Idx]); } } if (GetOptions()->GetValue(OPT_AdjustDateTz, i)) Mail::AdjustDateTz = i.CastInt32() == 0; // SSL debug logging if (GetOptions()->GetValue(OPT_DebugSSL, i)) SslSocket::DebugLogging = i.CastInt32() != 0; // Html edit menu if (GetOptions()->GetValue(OPT_EditControl, i)) { auto mi = Menu->FindItem(IDM_HTML_EDITOR); if (mi) mi->Checked(i.CastInt32() != 0); } } delete dlg; }); break; } case IDM_WORK_OFFLINE: { if (WorkOffline) { WorkOffline->Checked(!WorkOffline->Checked()); LVariant v; GetOptions()->SetValue(OPT_WorkOffline, v = WorkOffline->Checked()); if (!WorkOffline->Checked()) { // Offline -> Online transition. // Check if any pending messages are in the Outbox ScribeFolder *Outbox = GetFolder(FOLDER_OUTBOX); if (Outbox) { bool HasMailToSend = false; for (auto t: Outbox->Items) { Mail *m = t->IsMail(); if (m) { if (TestFlag(m->GetFlags(), MAIL_READY_TO_SEND)) { HasMailToSend = true; break; } } } if (HasMailToSend) { PostEvent(M_COMMAND, IDM_SEND_MAIL, #ifndef __GTK_H__ (LMessage::Param)Handle() #else 0 #endif ); } } } } break; } case IDM_ITEM_FILTER: { if (GetCtrlValue(IDM_ITEM_FILTER)) { if ((SearchView = new LSearchView(this))) { SearchView->Focus(true); SetLayout(); } } else { DeleteObj(SearchView); } ScribeFolder *Folder = GetCurrentFolder(); if (Folder) { Folder->Populate(MailList); } break; } case IDM_PRINT: { if (MailList) { List Sel; if (MailList->LList::GetSelection(Sel)) { for (auto i: Sel) { ThingType *t = dynamic_cast(i); ThingPrint(NULL, t); } } } break; } case IDM_PRINTSETUP: { auto *p = GetPrinter(); if (p && p->Browse(this)) { LString Str; if (p->Serialize(Str, true)) { LVariant v; GetOptions()->SetValue(OPT_PrintSettings, v = Str); } } break; } case IDM_PAGE_SETUP: { auto Dlg = new ScribePageSetup(this, GetOptions()); Dlg->DoModal(NULL); break; } case IDM_EXIT: { LMouse m; GetMouse(m); d->IngoreOnClose = m.Ctrl(); LCloseApp(); break; } // Edit menu case IDM_FIND: { auto v = GetFocus(); LDocView *doc = dynamic_cast(v); if (doc) { doc->DoFind(NULL); } else { ScribeFolder *Folder = GetCurrentFolder(); OpenFinder(this, Folder); } break; } case IDM_COPY: { if (MailList && MailList->Focus()) { List Lst; if (!MailList->GetSelection(Lst)) break; // Copy 'ScribeThingList' ScribeClipboardFmt *tl = ScribeClipboardFmt::Alloc(Lst); if (!tl) break; LClipBoard Clip(this); if (Clip.IsOpen()) { if (!Clip.Binary(d->ClipboardFormat, (uchar*)tl, tl->Sizeof(), true)) { LgiMsg(this, "Couldn't set the clipboard data.", AppName, MB_OK); } } else { LgiMsg(this, "Couldn't open the clipboard.", AppName, MB_OK); } free(tl); } else { LViewI *v = LAppInst->GetFocus(); if (v) v->PostEvent(M_COPY); } break; } case IDM_PASTE: { LViewI *v = LAppInst->GetFocus(); if (v && v->GetWindow() != (LWindow*)this) { v->PostEvent(M_PASTE); break; } if (!MailList->Focus() && !Tree->Focus()) { LgiTrace("%s:%i - List/Tree doesn't have focus.\n"); break; } ScribeFolder *Folder = dynamic_cast(Tree->Selection()); if (!Folder || !Folder->GetObject()) { LgiMsg(this, "No current folder.", AppName, MB_OK); break; } LClipBoard Clip(this); if (!Clip.IsOpen()) { LgiMsg(this, "Couldn't open the clipboard.", AppName, MB_OK); break; } LAutoPtr Data; ssize_t Size = 0; if (!Clip.Binary(d->ClipboardFormat, Data, &Size)) { LgiMsg(this, "Couldn't get the clipboard data.", AppName, MB_OK); break; } if (ScribeClipboardFmt::IsThing(Data.Get(), Size)) { new ScribePasteState(this, Folder, Data, Size); } break; } case IDM_DELETE: { LViewI *f = LAppInst->GetFocus(); LEdit *e = dynamic_cast(f); if (e) { // This handles the case where on a mac the menu eats the delete key, even // when the edit control needs it LKey k(LK_DELETE, 0); k.Down(true); f->OnKey(k); k.Down(false); f->OnKey(k); } else { OnDelete(); } break; } case IDM_DELETE_AS_SPAM: { if (MailList) { List Sel; MailList->GetSelection(Sel); int Index = -1; for (auto i: Sel) { Mail *m = IsMail(i); if (m) { if (Index < 0) { Index = MailList->IndexOf(i); } m->DeleteAsSpam(this); } } if (Index >= 0) { LListItem *i = MailList->ItemAt(Index); if (!i) i = MailList->ItemAt(MailList->Length()-1); if (i) i->Select(true); } } break; } case IDM_REFRESH: { ScribeFolder *f = GetCurrentFolder(); if (!f) break; const char *s = DomToStr(SdRefresh); f->GetFldObj()->OnCommand(s); break; } // Mail menu case IDM_NEW_EMAIL: { CreateMail(); break; } case IDM_SET_READ: case IDM_SET_UNREAD: { ScribeFolder *f = GetCurrentFolder(); if (!f) break; bool SetRead = Cmd == IDM_SET_READ; f->LoadThings(); LArray Change; for (auto t: f->Items) { Mail *m = t->IsMail(); if (m && m->Select()) Change.Add(m->GetObject()); } LVariant v = MAIL_READ; LDataStoreI *Store = f->GetObject()->GetStore(); if (Store->Change(Change, FIELD_FLAGS, v, SetRead ? OpPlusEquals : OpMinusEquals) == Store3Error) { for (auto t : f->Items) { Mail *m = t->IsMail(); if (!m) continue; if (!m->Select()) continue; if (SetRead) m->SetFlags(m->GetFlags() | MAIL_READ); else m->SetFlags(m->GetFlags() & ~MAIL_READ); } } break; } case IDM_REPLY: case IDM_REPLY_ALL: { if (MailList) MailReplyTo(IsMail(MailList->GetSelected()), (Cmd == IDM_REPLY_ALL)); break; } case IDM_FORWARD: { if (MailList) MailForward(IsMail(MailList->GetSelected())); break; } case IDM_BOUNCE: { if (MailList) MailBounce(IsMail(MailList->GetSelected())); break; } case IDM_SEND_MAIL: { Send(); break; } case IDM_RECEIVE_AND_SEND: { d->SendAfterReceive = true; PostEvent(M_COMMAND, IDM_RECEIVE_MAIL, (LMessage::Param)FindControl(IDM_RECEIVE_MAIL)); break; } case IDM_THREAD: { if (MailList) { ScribeFolder *f = GetCurrentFolder(); if (f) { f->SetThreaded(!f->GetThreaded()); f->Populate(MailList); } } break; } case IDM_RECEIVE_ALL: { #define LOG_RECEIVE_ALL 0 int i = 0; Accounts.Sort(AccountCmp); for (auto a : Accounts) { #if LOG_RECEIVE_ALL auto name = a->Identity.Name(); auto email = a->Identity.Email(); LString desc; desc.Printf("%s/%s", name.Str(), email.Str()); #endif if (!a->Receive.IsConfigured()) { #if LOG_RECEIVE_ALL LgiTrace("%s:%i - %i/%s not configured.\n", _FL, a->GetIndex(), desc.Get()); #endif } else if (a->Receive.Disabled() > 0) { #if LOG_RECEIVE_ALL LgiTrace("%s:%i - %i/%s is disabled.\n", _FL, a->GetIndex(), desc.Get()); #endif } else { #if LOG_RECEIVE_ALL LgiTrace("%s:%i - %i/%s will connect.\n", _FL, a->GetIndex(), desc.Get()); #endif Receive(a->GetIndex()); } i++; } break; } case IDM_RECEIVE_MAIL: { LVariant Def; if (GetOptions()->GetValue(OPT_Pop3DefAction, Def) && Def.CastInt32() == 0) return OnCommand(IDM_RECEIVE_ALL, 0, NULL); Receive(0); break; } case IDM_PREVIEW_POP3: { LArray Account; Accounts.Sort(AccountCmp); for (auto a: Accounts) { if (!a->Receive.IsConfigured()) continue; auto Protocol = ProtocolStrToEnum(a->Receive.Protocol().Str()); if (Protocol == ProtocolPop3) { Account.Add(a); break; } } if (Account.Length() == 1) OpenPopView(this, Account); break; } case IDM_CALENDAR: { extern void OpenCalender(ScribeFolder *folder); ScribeFolder *Folder = GetFolder(FOLDER_CALENDAR); if (Folder) { OpenCalender(Folder); } break; } // Contact menu case IDM_NEW_CONTACT: { CreateItem(MAGIC_CONTACT, NULL); break; } case IDM_NEW_GROUP: { CreateItem(MAGIC_GROUP, NULL); break; } // Filter menu case IDM_NEW_FILTER: { Thing *t = CreateItem(MAGIC_FILTER, NULL, false); if (t) { t->IsFilter()->SetIncoming(true); t->DoUI(); } break; } case IDM_FILTER_CURRENT_FOLDER: { ScribeFolder *Folder = GetCurrentFolder(); if (Folder) { List Filters; GetFilters(Filters, false, false, true); List Src; for (auto i: Folder->Items) { if (i->IsMail()) { Src.Insert(i->IsMail()); } } if (!Src[0]) { LgiMsg(this, LLoadString(IDS_NO_MAIL_TO_FILTER), AppName); } else { Filter::ApplyFilters(this, Filters, Src); } } break; } case IDM_FILTER_SELECTION: { ScribeFolder *Folder = GetCurrentFolder(); if (Folder) { List Filters; GetFilters(Filters, false, false, true); List Src; for (auto i: Folder->Items) { if (i->IsMail() && i->Select()) { Src.Insert(i->IsMail()); } } if (Src.Length()) { Filter::ApplyFilters(this, Filters, Src); } } break; } case IDM_DEBUG_FILTERS: { auto i = Menu->FindItem(IDM_DEBUG_FILTERS); if (i) { i->Checked(!i->Checked()); } break; } case IDM_HTML_EDITOR: { auto i = Menu->FindItem(IDM_HTML_EDITOR); if (i) { i->Checked(!i->Checked()); LVariant v; GetOptions()->SetValue(OPT_EditControl, v = i->Checked() ? 1 : 0); } break; } case IDM_FILTERS_DISABLE: { if (d->DisableUserFilters) { d->DisableUserFilters->Checked(!d->DisableUserFilters->Checked()); LVariant v; GetOptions()->SetValue(OPT_DisableUserFilters, v = d->DisableUserFilters->Checked()); } break; } case IDM_BUILD_BAYES_DB: { BuildSpamDb(); break; } case IDM_BAYES_STATS: { BuildStats(); break; } case IDM_BAYES_SETTINGS: { auto Dlg = new BayesDlg(this); Dlg->DoModal([this, Dlg](auto dlg, auto id) { if (id) { LVariant i; if (GetOptions()->GetValue(OPT_BayesFilterMode, i)) { ScribeBayesianFilterMode m = ((ScribeBayesianFilterMode)i.CastInt32()); if (m != BayesOff) { LVariant SpamPath, ProbablyPath; GetOptions()->GetValue(OPT_SpamFolder, SpamPath); GetOptions()->GetValue(OPT_BayesMoveTo, ProbablyPath); if (m == BayesFilter) { ScribeFolder *Spam = GetFolder(SpamPath.Str()); if (!Spam) { LMailStore *RelevantStore = GetMailStoreForPath(SpamPath.Str()); if (RelevantStore) { LString p = SpamPath.Str(); LString::Array a = p.SplitDelimit("/"); Spam = RelevantStore->Root; for (unsigned i=1; iGetSubFolder(a[i]); if (!c) c = Spam->CreateSubDirectory(a[i], MAGIC_MAIL); Spam = c; } } } if (Spam) { LVariant v; GetOptions()->SetValue(OPT_HasSpam, v = 1); } } else if (m == BayesTrain) { ScribeFolder *Probably = GetFolder(ProbablyPath.Str()); if (!Probably) { LgiMsg(this, "Couldn't find the folder '%s'", AppName, MB_OK, ProbablyPath.Str()); } } } } } delete dlg; }); break; } case IDM_BAYES_CHECK: { List Sel; if (MailList) MailList->GetSelection(Sel); for (auto i: Sel) { Thing *t = dynamic_cast(i); if (t) { Mail *m = t->IsMail(); if (m) { d->BayesLog.Empty(); double SpamRating = 0.0; IsSpam(SpamRating, m, true); break; } } } break; } // Tools menu case IDM_SCRIPTING_CONSOLE: case IDM_SHOW_CONSOLE: { ShowScriptingConsole(); if (d->ShowConsoleBtn) d->ShowConsoleBtn->Image(IMG_CONSOLE_NOMSG); break; } case IDM_EXPORT_TEXT_MBOX: { Export_UnixMBox(this); break; } case IDM_IMPORT_CSV: { ImportCsv(this); break; } case IDM_EXPORT_CSV: { ExportCsv(this); break; } case IDM_IMPORT_EML: { ImportEml(this); break; } case IDM_EXPORT_SCRIBE: { ExportScribe(this, NULL/* default mail store */); break; } case IDM_IMPORT_TEXT_MBOX: { Import_UnixMBox(this); break; } case IDM_IMP_EUDORA_ADDR: { Import_EudoraAddressBook(this); break; } case IDM_IMP_MOZILLA_ADDR: { Import_MozillaAddressBook(this); break; } case IDM_IMP_MOZILLA_MAIL: { Import_MozillaMail(this); break; } #if WINNATIVE case IDM_IMPORT_OUTLOOK_PAB: { Import_OutlookContacts(this); break; } case IDM_IMPORT_OUTLOOK_ITEMS: { Import_Outlook(this, IMP_OUTLOOK); break; } case IDM_EXPORT_OUTLOOK_ITEMS: { Export_Outlook(this); break; } #endif case IDM_IMP_MBX_EMAIL: { Import_OutlookExpress(this, false); // v4 break; } case IDM_IMP_DBX_EMAIL: { Import_OutlookExpress(this); // v5 break; } case IDM_IMPORT_NS_CONTACTS: { Import_NetscapeContacts(this); break; } case IDM_CHECK_UPDATE: { LVariant v; GetOptions()->GetValue(OPT_SoftwareUpdateIncBeta, v); SoftwareUpdate(this, true, v.CastInt32() != 0, [](auto goingToUpdate) { if (goingToUpdate) LCloseApp(); }); break; } case IDM_LOGOUT: { CurrentAuthLevel = PermRequireNone; auto i = Menu->FindItem(IDM_LOGOUT); if (i) i->Enabled(false); break; } case IDM_LAYOUT1: { LVariant v; int TwoThirds = GetClient().Y() >> 1; GetOptions()->SetValue(OPT_SplitterPos, v = 200); GetOptions()->SetValue(OPT_SubSplitPos, v = TwoThirds); SetLayout(FoldersListAndPreview); break; } case IDM_LAYOUT2: { LVariant v; int TwoThirds = GetClient().Y() >> 1; GetOptions()->SetValue(OPT_SplitterPos, v = TwoThirds); GetOptions()->SetValue(OPT_SubSplitPos, v = 200); SetLayout(PreviewOnBottom); break; } case IDM_LAYOUT3: { LVariant v; GetOptions()->SetValue(OPT_SplitterPos, v = 200); SetLayout(FoldersAndList); break; } case IDM_LAYOUT4: { LVariant v; GetOptions()->SetValue(OPT_SplitterPos, v = 200); GetOptions()->SetValue(OPT_SubSplitPos, v); SetLayout(ThreeColumn); break; } case IDM_CRASH: { int *Crash = 0; *Crash = true; break; } case IDM_DUMP_MEM: { LDumpMemoryStats(0); break; } case IDM_SCRIPT_DEBUG: { LVariant v; if (GetOptions()) GetOptions()->SetValue(OPT_ScriptDebugger, v = true); LVirtualMachine *vm = new LVirtualMachine(d); if (!vm) break; LVmDebugger *dbg = vm->OpenDebugger(); if (!dbg) break; dbg->OwnVm(true); break; } case IDM_SCRIPT_BREAK_ON_WARN: { auto mi = GetMenu()->FindItem(IDM_SCRIPT_BREAK_ON_WARN); if (!mi) break; LVirtualMachine::BreakOnWarning = !mi->Checked(); mi->Checked(LVirtualMachine::BreakOnWarning); break; } // Help menu case IDM_HELP: { LaunchHelp("index.html"); // LgiMsg(this, LLoadString(IDS_ERROR_NO_HELP), AppName, MB_OK); break; } case IDM_FEEDBACK: { LVariant e; if (GetOptions()->GetValue("author", e)) CreateMail(0, e.Str()); else CreateMail(0, AuthorEmailAddr); break; } case IDM_MEMECODE: { LExecute(AuthorHomepage); break; } case IDM_HOMEPAGE: { LVariant hp; if (GetOptions()->GetValue("homepage", hp)) LExecute(hp.Str()); else LExecute(ApplicationHomepage); break; } case IDM_VERSION_HISTORY: { LExecute("http://www.memecode.com/site/ver.php?id=445"); break; } case IDM_DEBUG_INFO: { char s[256]; sprintf_s(s, sizeof(s), "%s#debug", ApplicationHomepage); LExecute(s); break; } case IDM_TUTORIALS: { LExecute("http://www.memecode.com/scribe/tutorials"); break; } case IDM_INSCRIBE_LINK: { LExecute(CommercialHomepage); break; } case IDM_SCRIBE_FAQ: { LExecute(FaqHomepage); break; } case IDM_ABOUT: { extern void ScribeAbout(ScribeWnd *Parent); ScribeAbout(this); break; } default: { if (d->ScriptToolbar) d->ScriptToolbar->ExecuteCallbacks(this, 0, 0, Cmd); break; } } return 0; } void ScribeWnd::OnDelete() { LVariant ConfirmDelete; GetOptions()->GetValue(OPT_ConfirmDelete, ConfirmDelete); if (!ConfirmDelete.CastInt32() || LgiMsg(this, LLoadString(IDS_DELETE_ASK), AppName, MB_YESNO) == IDYES) { LArray Del; if (Tree && Tree->Focus()) { ScribeFolder *Item = dynamic_cast(Tree->Selection()); if (Item) { Tree->OnDelete(Item, false); } } else if (MailList #ifdef MAC && MailList->Focus() #endif ) { List Sel; MailList->GetSelection(Sel); for (auto i: Sel) { Thing *t = dynamic_cast(i); if (t) Del.Add(t->GetObject()); } if (Del.Length()) { auto Store = Del[0]->GetStore(); Store->Delete(Del, true); } else LgiTrace("%s:%i - Nothing to delete\n", _FL); #ifndef MAC MailList->Focus(true); #endif } } } int ScribeWnd::OnNotify(LViewI *Ctrl, LNotification n) { switch (Ctrl->GetId()) { case IDC_THING_LIST: { if (n.Type == LNotifyReturnKey) { LListItem *i = MailList ? MailList->GetSelected() : 0; Thing *t = dynamic_cast(i); if (t) { t->DoUI(); } } else if (n.Type == LNotifyDeleteKey) { /* This is now handled by the menu OnDelete(); return true; */ } if (SearchView && MailList) { SearchView->OnNotify(Ctrl, n); } break; } case IDC_TEXT: { if (PreviewPanel) { PreviewPanel->OnNotify(Ctrl, n); } break; } } return 0; } void ScribeWnd::AddThingSrc(ScribeFolder *src) { if (!d->ThingSources.HasItem(src)) d->ThingSources.Add(src); } void ScribeWnd::RemoveThingSrc(ScribeFolder *src) { d->ThingSources.Delete(src); } LArray ScribeWnd::GetThingSources(Store3ItemTypes Type) { LArray a; for (auto f: d->ThingSources) { if (f->GetItemType() == Type && !f->IsInTrash()) { a.Add(f); } } return a; } bool ScribeWnd::LogFilterActivity() { auto i = Menu->FindItem(IDM_DEBUG_FILTERS); return i ? i->Checked() : false; } bool ScribeWnd::CreateFolders(LAutoString &FileName) { bool Status = false; if (FileName) { char *Ext = LGetExtension(FileName); if (!Ext) { char File[300]; strcpy_s(File, sizeof(File), FileName); strcat(File, ".mail3"); FileName.Reset(NewStr(File)); } // Create objects, and then close the file.. it'll be reloaded later LAutoPtr m(CreateDataStore(FileName, true)); if (m) { m->GetRoot(true); Status = true; } else LgiTrace("%s:%i - CreateDataStore failed.\n", _FL); } else LgiTrace("%s:%i - No file name for CreateFolder.\n", _FL); return Status; } bool ScribeWnd::CompactFolders(LMailStore &Store, bool Interactive) { if (!Store.Store) return false; auto Dlg = new Store3Progress(this, Interactive); Dlg->SetDescription(LLoadString(IDS_CHECKING_OBJECTS)); bool Offline = false; if (WorkOffline) { Offline = WorkOffline->Checked(); WorkOffline->Checked(true); } Store.Store->Compact(this, Dlg, [this, Offline, Dlg](auto status) { LAssert(InThread()); if (WorkOffline) WorkOffline->Checked(Offline); delete Dlg; }); return true; } CalendarSource *CalendarSource::Create(ScribeWnd *App, const char *ObjName, const char *Id) { if (!Stricmp(ObjName, "RemoteCalendarSource")) return new RemoteCalendarSource(App, Id); return new FolderCalendarSource(App, Id); } int ScribeWnd::GetCalendarSources(LArray &Out) { static bool Loaded = false; if (!Loaded) { Loaded = true; CalendarSource::SetCreateIn(NULL); LVariant Create; GetOptions()->GetValue(OPT_CalendarCreateIn, Create); // This should be a list of all calendar folders in ANY mail store... auto CalFlds = GetThingSources(MAGIC_CALENDAR); LXmlTag *t = GetOptions()->LockTag(OPT_CalendarSources, _FL); if (t) { bool AutoPopulate = t->Children.Length() == 0; for (auto c: t->Children) { auto s = CalendarSource::Create(this, c->GetAttr(CalendarSource::OptObject), c->GetTag()); if (s && s->Read()) { // Add known source... if (!Stricmp(Create.Str(), c->GetTag())) { CalendarSource::SetCreateIn(s); } // Remove from CalFlds FolderCalendarSource *Fcs = dynamic_cast(c); if (Fcs) { auto Path = Fcs->GetPath(); for (auto c: CalFlds) { if (c->GetPath().Equals(Path)) { CalFlds.Delete(c); break; } } } } } if (AutoPopulate) { // Now CalFlds should be a list of all calendar folders NOT in the source XML tag for (auto c: CalFlds) { FolderCalendarSource *s = new FolderCalendarSource(this); if (s) { // So add an entry to track it... auto Path = c->GetPath(); s->SetPath(Path); s->SetDisplay(true); s->SetColour(CalendarSource::FindUnusedColour()); s->Write(); } } } GetOptions()->Unlock(); } if (!CalendarSource::GetCreateIn() && CalendarSource::GetSources().Length()) { CalendarSource::SetCreateIn(CalendarSource::GetSources().ItemAt(0)); } } for (unsigned i=0; iPathOption; fi++) { bool Check = true; if (fi->HasOption) { LVariant v; if (GetOptions()->GetValue(fi->HasOption, v)) Check = v.CastInt32() != 0; } if (Check) { ScribeFolder *c = GetFolder(fi->Id); if (c == f) return fi->Id; } } return -1; } ScribeFolder *ScribeWnd::GetCurrentFolder() { if (Tree) { auto *Item = Tree->Selection(); if (Item) { return dynamic_cast(Item); } } return 0; } bool ScribeWnd::GetSystemPath(int Folder, LVariant &Path) { char KeyName[64]; sprintf_s(KeyName, sizeof(KeyName), "Folder-%i", Folder); return GetOptions()->GetValue(KeyName, Path); } LMailStore *ScribeWnd::GetMailStoreForIdentity(const char *IdEmail) { LVariant Tmp; if (!IdEmail) { // Get current identity ScribeAccount *Cur = GetCurrentAccount(); if (Cur) { Tmp = Cur->Identity.Email(); IdEmail = Tmp.Str(); } } if (!IdEmail) return NULL; ScribeAccount *a = NULL; for (auto Acc : Accounts) { LVariant e = Acc->Identity.Email(); if (e.Str() && !_stricmp(e.Str(), IdEmail)) { a = Acc; break; } } if (!a) return NULL; LVariant DestPath = a->Receive.DestinationFolder(); if (!DestPath.Str()) return NULL; return GetMailStoreForPath(DestPath.Str()); } ScribeFolder *ScribeWnd::GetFolder(int Id, LDataI *s) { if (s) { for (auto &f: Folders) if (s->GetStore() == f.Store) return GetFolder(Id, &f); } return GetFolder(Id); } ScribeFolder *ScribeWnd::GetFolder(int Id, LMailStore *Store, bool Quiet) { char KeyName[64]; sprintf_s(KeyName, sizeof(KeyName), "Folder-%i", Id); LVariant FolderName; bool NoOption = false; if (GetOptions()->GetValue(KeyName, FolderName)) { if (ValidStr(FolderName.Str()) && strlen(FolderName.Str()) > 0) { ScribeFolder *c = GetFolder(FolderName.Str(), Store); if (c) { return c; } else if (!Quiet) { LgiTrace("%s:%i - '%s' doesn't exist.\n", _FL, FolderName.Str()); } } } else if (!Quiet) { // LgiTrace("%s:%i - No option '%s'\n", _FL, KeyName); NoOption = true; } switch (Id) { case FOLDER_INBOX: case FOLDER_OUTBOX: case FOLDER_SENT: case FOLDER_TRASH: case FOLDER_CONTACTS: case FOLDER_TEMPLATES: case FOLDER_FILTERS: case FOLDER_CALENDAR: case FOLDER_GROUPS: case FOLDER_SPAM: { ScribeFolder *c = GetFolder(DefaultFolderNames[Id], Store); if (!c) { // if (!Quiet) // LgiTrace("%s:%i - Default folder '%s' doesn't exist.\n", _FL, DefaultFolderNames[Id]); } else if (NoOption) { auto p = c->GetPath(); GetOptions()->SetValue(KeyName, FolderName = p.Get()); } return c; } } return NULL; } bool ScribeWnd::OnMailStore(LMailStore **MailStore, bool Add) { if (!MailStore) { LAssert(!"No mail store pointer?"); return false; } if (Add) { *MailStore = &Folders.New(); if (*MailStore) return true; } else { ssize_t Idx = *MailStore - &Folders[0]; if (Idx >= 0 && Idx < (ssize_t)Folders.Length()) { Folders.DeleteAt(Idx, true); *MailStore = NULL; return true; } else { LAssert(!"Index out of range."); } } return false; } LMailStore *ScribeWnd::GetMailStoreForPath(const char *Path) { if (!Path) return NULL; - LToken t(Path, "/"); + auto t = LString(Path).SplitDelimit("/"); if (t.Length() > 0) { const char *First = t[0]; // Find the mail store that that t[0] refers to for (unsigned i=0; iGetText(); if (RootStr && !_stricmp(RootStr, First)) { return &Folders[i]; } } } } return NULL; } ScribeFolder *ScribeWnd::GetFolder(const char *Name, LMailStore *s) { ScribeFolder *Folder = 0; if (ValidStr(Name)) { LString Sep("/"); auto t = LString(Name).Split(Sep); LMailStore tmp; LString TmpName; if (t.Length() > 0) { if (!s) { s = GetMailStoreForPath(Name); if (!s) { // IMAP folders? for (auto a: Accounts) { ScribeProtocol Proto = a->Receive.ProtocolType(); if (Proto == ProtocolImapFull) { ScribeFolder *Root = a->Receive.GetRootFolder(); if (Root) { const char *RootStr = Root->GetText(); if (RootStr && a->Receive.GetDataStore() && !_stricmp(RootStr, t[0])) { tmp.Root = Root; tmp.Store = a->Receive.GetDataStore(); s = &tmp; break; } } } } } if (s) { if (*Name == '/') Name++; Name = strchr(Name, '/'); if (!Name) Name = "/"; } } else if (s->Root) { // Check if the store name is on the start of the folder auto RootName = s->Root->GetName(true); if (RootName.Equals(t[0])) { LString::Array a; for (unsigned i=1; iRoot; Folder = s->Root ? s->Root->GetSubFolder(Name) : 0; } } return Folder; } void ScribeWnd::Update(int What) { if (What & UPDATE_TREE) { Tree->Invalidate(); return; } if (What & UPDATE_LIST) { if (MailList) MailList->Invalidate(); return; } } void ScribeWnd::DoDebug(char *s) { } Thing *ScribeWnd::CreateThingOfType(Store3ItemTypes Type, LDataI *obj) { Thing *t = NULL; switch (Type) { case MAGIC_CONTACT: { t = new Contact(this, obj); break; } case MAGIC_MAIL: { t = new Mail(this, obj); break; } case MAGIC_ATTACHMENT: { t = new Attachment(this, obj); break; } case MAGIC_FILTER: { t = new Filter(this, obj); break; } case MAGIC_CALENDAR: { t = new Calendar(this, obj); break; } case MAGIC_GROUP: { t = new ContactGroup(this, obj); break; } default: break; } if (t) { t->App = this; } return t; } void ScribeWnd::GetFilters(List &Filters, bool JustIn, bool JustOut, bool JustInternal) { auto Srcs = GetThingSources(MAGIC_FILTER); for (auto f: Srcs) { for (auto t: f->Items) { Filter *Ftr = t->IsFilter(); if (Ftr) { if (JustIn && !Ftr->GetIncoming()) continue; if (JustOut && !Ftr->GetOutgoing()) continue; if (JustInternal && !Ftr->GetInternal()) continue; Filters.Insert(Ftr); } } } extern int FilterCompare(Filter *a, Filter *b, NativeInt Data); Filters.Sort(FilterCompare); } bool ScribeWnd::ShowToolbarText() { LVariant i; if (GetOptions()->GetValue(OPT_ToolbarText, i)) { return i.CastInt32() != 0; } GetOptions()->SetValue(OPT_ToolbarText, i = true); return true; } void ScribeWnd::HashContacts(LHashTbl,Contact*> &Contacts, ScribeFolder *Folder, bool Deep) { if (!Folder) { // Default item is the contacts folder Folder = GetFolder(FOLDER_CONTACTS); // Also look at all the contact sources... auto Srcs = GetThingSources(MAGIC_CONTACT); for (auto Src: Srcs) { for (auto t: Src->Items) { Contact *c = t->IsContact(); if (!c) continue; auto emails = c->GetEmails(); for (auto e: emails) { if (!Contacts.Find(e)) Contacts.Add(e, c); } } } } // recurse through each folder and make a list // of every contact object we find. if (Folder) { Folder->LoadThings(); for (auto t: Folder->Items) { Contact *c = t->IsContact(); if (c) { auto Emails = c->GetEmails(); for (auto e: Emails) if (e && !Contacts.Find(e)) Contacts.Add(e, c); } } for (auto f = Folder->GetChildFolder(); Deep && f; f = f->GetNextFolder()) { HashContacts(Contacts, f, Deep); } } } List *ScribeWnd::GetEveryone() { return &Contact::Everyone; } bool ScribeWnd::GetContacts(List &Contacts, ScribeFolder *Folder, bool Deep) { LArray Folders; if (!Folder) { Folders = GetThingSources(MAGIC_CONTACT); auto f = GetFolder(FOLDER_CONTACTS); if (f && !Folders.HasItem(f)) Folders.Add(f); } else Folders.Add(Folder); if (!Folders.Length()) return false; for (auto f: Folders) { // recurse through each folder and make a list // of every contact object we find. ScribePerm Perm = f->GetFolderPerms(ScribeReadAccess); bool Safe = CurrentAuthLevel >= Perm; if (Safe) { f->LoadThings(); for (auto t: f->Items) { Contact *c = t->IsContact(); if (c) Contacts.Insert(c); } for (ScribeFolder *c = f->GetChildFolder(); Deep && c; c = c->GetNextFolder()) GetContacts(Contacts, c, Deep); } } return true; } /* This function goes through the database and checks for some basic requirements and fixes things up if they aren't ok. */ bool ScribeWnd::ValidateFolder(LMailStore *s, int Id) { char OptName[32]; sprintf_s(OptName, sizeof(OptName), "Folder-%i", Id); LVariant Path; if (!GetOptions()->GetValue(OptName, Path)) { char Opt[256]; sprintf_s(Opt, sizeof(Opt), "/%s", DefaultFolderNames[Id]); GetOptions()->SetValue(OptName, Path = Opt); } // If the path name has the store name at the start, strip that off... LString Sep("/"); LString::Array Parts = LString(Path.Str()).Split(Sep); if (Parts.Length() > 1) { if (Parts[0].Equals(s->Name)) { Parts.DeleteAt(0, true); Path = Sep.Join(Parts); } else { LMailStore *ms = GetMailStoreForPath(Path.Str()); if (ms) { s = ms; } else { // Most likely the user has renamed something and broken the // path. Lets just error out instead of creating the wrong folder return false; } } } // Now resolve the path... ScribeFolder *Folder = GetFolder(Path.Str(), s); if (!Folder) { char *p = Path.Str(); if (_strnicmp(p, "/IMAP ", 6) != 0) { LAssert(DefaultFolderTypes[Id] != MAGIC_NONE); Folder = s->Root->CreateSubDirectory(*p=='/'?p+1:p, DefaultFolderTypes[Id]); } } if (!Folder) return false; Folder->SetDefaultFields(); return true; } void ScribeWnd::Validate(LMailStore *s) { // Check for all the basic folders int Errors = 0; for (SystemFolderInfo *fi = SystemFolders; fi->PathOption; fi++) { bool Check = true; if (fi->HasOption) { LVariant v; if (GetOptions()->GetValue(fi->HasOption, v)) Check = v.CastInt32() != 0; } if (Check) { if (!ValidateFolder(s, fi->Id)) Errors++; } } if (Errors && LgiMsg(this, "There were errors validating the system folders." "Would you like to review the mail store's system folder paths?", AppName, MB_YESNO) == IDYES) { PostEvent(M_COMMAND, IDM_MANAGE_MAIL_STORES); } } ThingFilter *ScribeWnd::GetThingFilter() { return SearchView; } ScribeAccount *ScribeWnd::GetSendAccount() { LVariant DefSendAcc = 0; if (!GetOptions()->GetValue(OPT_DefaultSendAccount, DefSendAcc)) { for (auto a : Accounts) if (a->Send.Server().Str()) return a; } ScribeAccount *i = Accounts.ItemAt(DefSendAcc.CastInt32()); if (i && i->Send.Server().Str()) return i; return NULL; } LPrinter *ScribeWnd::GetPrinter() { if (!d->PrintOptions) d->PrintOptions.Reset(new LPrinter); return d->PrintOptions; } int ScribeWnd::GetActiveThreads() { int Status = 0; for (auto i: Accounts) { if (i->IsOnline()) { Status++; } } return Status; } class DefaultClientDlg : public LDialog { public: bool DontWarn; DefaultClientDlg(LView *parent) { DontWarn = false; SetParent(parent); LoadFromResource(IDD_WARN_DEFAULT); MoveToCenter(); } int OnNotify(LViewI *Ctrl, LNotification n) { switch (Ctrl->GetId()) { case ID_YES: case ID_NO: { LCheckBox *DW; if (GetViewById(IDC_DONT_WARN, DW)) { DontWarn = DW->Value() != 0; } EndModal(Ctrl->GetId() == ID_YES); break; } } return 0; } }; #if WINNATIVE struct DefaultClient { char DefIcon[MAX_PATH_LEN]; char CmdLine[MAX_PATH_LEN]; char DllPath[MAX_PATH_LEN]; DefaultClient() { auto Exe = LGetExeFile(); sprintf_s(DefIcon, sizeof(DefIcon), "%s,1", Exe.Get()); sprintf_s(CmdLine, sizeof(CmdLine), "\"%s\" /m \"%%1\"", Exe.Get()); LMakePath(DllPath, sizeof(DllPath), Exe, "../ScribeMapi.dll"); } bool IsWindowsXp() { LArray Ver; int Os = LGetOs(&Ver); if ( ( Os == LGI_OS_WIN32 || Os == LGI_OS_WIN64 ) && Ver.Length() > 1 && Ver[0] == 5 && Ver[1] == 1 ) return true; return false; } bool InstallMailto(bool Write) { LAutoPtr mailto = CheckKey(Write, "HKCR\\mailto"); if (!mailto) return false; if (!CheckString(Write, mailto, NULL, "URL:MailTo Protocol")) return false; LAutoPtr deficon = CheckKey(Write, "HKCR\\mailto\\DefaultIcon"); if (!deficon) return false; if (!CheckString(Write, deficon, NULL, DefIcon)) return false; LAutoPtr shell = CheckKey(Write, "HKCR\\mailto\\shell"); if (!shell) return false; if (!CheckString(Write, shell, NULL, "open")) return false; LAutoPtr cmd = CheckKey(Write, "HKCR\\mailto\\shell\\open\\command"); if (!cmd) return false; if (!CheckString(Write, cmd, NULL, CmdLine)) return false; return true; } LAutoPtr CheckKey(bool Write, const char *Key, ...) const { char Buffer[512]; va_list Arg; va_start(Arg, Key); vsprintf_s(Buffer, sizeof(Buffer), Key, Arg); va_end(Arg); LAutoPtr k(new LRegKey(Write, Buffer)); if (k && Write && !k->IsOk()) { if (!k->Create()) { k.Reset(); LgiTrace("%s:%i - Failed to create '%s'\n", _FL, Buffer); } } return k; } bool CheckInt(bool Write, LRegKey *k, const char *Name, uint32_t Value) { if (!k) { LgiTrace("%s:%i - No key: '%s'\n", _FL, Name); return false; } uint32_t Cur; if (!k->GetInt(Name, Cur)) Cur = Value + 1; if (Cur == Value) return true; if (Write) { bool Status = k->SetInt(Name, Value); if (!Status) LgiTrace("%s:%i - Failed to set key '%s': '%s' to %i\n", _FL, k->Name(), Name, Value); return Status; } return false; } bool CheckString(bool Write, LRegKey *k, const char *StrName, const char *StrValue) { if (!k) { LgiTrace("%s:%i - No key: '%s' to '%s'\n", _FL, StrName, StrValue); return false; } LString v; if (k->GetStr(StrName, v)) { bool Same = Stricmp(v.Get(), StrValue) == 0; if (Write && !Same) { bool Status = k->SetStr(StrName, StrValue); if (!Status) LgiTrace("%s:%i - Failed to set key '%s': '%s' to '%s'\n", _FL, k->Name(), StrName, StrValue); return Status; } return Same; } else if (Write) { bool Status = k->SetStr(StrName, StrValue); if (!Status) LgiTrace("%s:%i - Failed to set key '%s': '%s' to '%s'\n", _FL, k->Name(), StrName, StrValue); return Status; } return false; } bool IsDefault() { LAutoPtr mail = CheckKey(false, "HKCU\\Software\\Clients\\Mail"); if (!mail) return false; LString v; if (!mail->GetStr(NULL, v)) return false; return !_stricmp(v, "Scribe"); } bool SetDefault() const { LAutoPtr mail = CheckKey(true, "HKCU\\Software\\Clients\\Mail"); if (!mail) return false; // Set the default client in the current user tree. mail->SetStr(NULL, "Scribe"); // Configure the mailto handler const char *Base = "HKEY_ROOT"; bool Error = false; LRegKey Mt(true, "%s\\mailto", Base); if (Mt.IsOk() || Mt.Create()) { if (!Mt.SetStr(0, "URL:MailTo Protocol") || !Mt.SetStr("URL Protocol", "")) Error = true; } else { LgiTrace("%s:%i - Couldn't open/create registry key (err=%i).\n", _FL, GetLastError()); Error = true; } LRegKey Di(true, "%s\\mailto\\DefaultIcon", Base); if (Di.IsOk() || Di.Create()) { if (!Di.SetStr(0, DefIcon)) Error = true; } else { LgiTrace("%s:%i - Couldn't open/create registry key (err=%i).\n", _FL, GetLastError()); Error = true; } LRegKey c(true, "%s\\mailto\\shell\\open\\command", Base); if (c.IsOk() || c.Create()) { if (!c.SetStr(NULL, CmdLine)) Error = true; } else { LgiTrace("%s:%i - Couldn't open/create registry key (err=%i).\n", _FL, GetLastError()); Error = true; } return Error; } bool InstallAsClient(char *Base, bool Write) { // Create software client entry, to put Scribe in the Internet Options for mail clients. LAutoPtr mail = CheckKey(Write, "%s\\Software\\Clients\\Mail", Base); if (!mail) return false; LAutoPtr app = CheckKey(Write, "%s\\Software\\Clients\\Mail\\Scribe", Base); if (!app) return false; if (!CheckString(Write, app, NULL, AppName)) return false; if (!CheckString(Write, app, "DllPath", DllPath)) return false; LAutoPtr shell = CheckKey(Write, "%s\\Software\\Clients\\Mail\\Scribe\\shell\\open\\command", Base); if (!shell) return false; if (!CheckString(Write, shell, NULL, CmdLine)) return false; LAutoPtr icon = CheckKey(Write, "%s\\Software\\Clients\\Mail\\Scribe\\DefaultIcon", Base); if (!icon) return false; if (!CheckString(Write, icon, NULL, DefIcon)) return false; LAutoPtr proto = CheckKey(Write, "%s\\Software\\Classes\\Protocol\\mailto", Base); if (!proto) return false; if (!CheckString(Write, proto, NULL, "URL:MailTo Protocol")) return false; if (!CheckString(Write, proto, "URL Protocol", "")) return false; if (!CheckInt(Write, proto, "EditFlags", 0x2)) return false; LAutoPtr proto_cmd = CheckKey(Write, "%s\\Software\\Classes\\Protocol\\mailto\\shell\\open\\command", Base); if (!proto_cmd) return false; if (!CheckString(Write, proto_cmd, NULL, CmdLine)) return false; return true; } struct FileType { char *Name; char *Desc; int Icon; }; static FileType FileTypes[]; bool Win7Install(bool Write) { // http://msdn.microsoft.com/en-us/library/windows/desktop/cc144154%28v=vs.85%29.aspx LArray Ver; int Os = LGetOs(&Ver); if ( ( Os == LGI_OS_WIN32 || Os == LGI_OS_WIN64 ) && Ver[0] >= 6) { char Path[MAX_PATH_LEN]; auto Exe = LGetExeFile(); for (int i=0; FileTypes[i].Name; i++) { LAutoPtr base = CheckKey(Write, "HKEY_CLASSES_ROOT\\%s", FileTypes[i].Name); if (!base) return false; if (!CheckString(Write, base, NULL, FileTypes[i].Desc)) return false; LAutoPtr r = CheckKey(Write, "HKEY_CLASSES_ROOT\\%s\\shell\\Open\\command", FileTypes[i].Name); if (!r) return false; sprintf_s(Path, sizeof(Path), "\"%s\" -u \"%%1\"", Exe.Get()); if (!CheckString(Write, r, NULL, Path)) return false; LAutoPtr ico = CheckKey(Write, "HKEY_CLASSES_ROOT\\%s\\DefaultIcon", FileTypes[i].Name); if (!ico) return false; sprintf_s(Path, sizeof(Path), "%s,%i", Exe.Get(), FileTypes[i].Icon); if (!CheckString(Write, ico, NULL, Path)) return false; } LAutoPtr r = CheckKey(Write, "HKEY_LOCAL_MACHINE\\SOFTWARE\\Clients\\Mail\\Scribe\\Capabilities"); if (!r) return false; if (!CheckString(Write, r, "ApplicationDescription", "Scribe is a small lightweight email client.") && !CheckString(Write, r, "ApplicationName", "Scribe") && !CheckString(Write, r, "ApplicationIcon", DefIcon)) return false; LAutoPtr as = CheckKey(Write, "HKEY_LOCAL_MACHINE\\SOFTWARE\\Clients\\Mail\\Scribe\\Capabilities\\FileAssociations"); if (!as) return false; if (!CheckString(Write, as, ".eml", "Scribe.Email") && !CheckString(Write, as, ".msg", "Scribe.Email") && !CheckString(Write, as, ".mbox", "Scribe.Folder") && !CheckString(Write, as, ".mbx", "Scribe.Folder") && !CheckString(Write, as, ".ics", "Scribe.Calendar") && !CheckString(Write, as, ".vcs", "Scribe.Calendar") && !CheckString(Write, as, ".vcf", "Scribe.Contact") && !CheckString(Write, as, ".mail3", "Scribe.MailStore")) return false; LAutoPtr ua = CheckKey(Write, "HKEY_LOCAL_MACHINE\\SOFTWARE\\Clients\\Mail\\Scribe\\Capabilities\\UrlAssociations"); if (!ua) return false; if (!CheckString(Write, ua, "mailto", "Scribe.Mailto")) return false; LAutoPtr a = CheckKey(Write, "HKEY_LOCAL_MACHINE\\SOFTWARE\\RegisteredApplications"); if (!a) return false; if (!CheckString(Write, a, "Scribe", "SOFTWARE\\Clients\\Mail\\Scribe\\Capabilities")) return false; } return true; } void Win7Uninstall() { for (int i=0; FileTypes[i].Name; i++) { LRegKey base(true, "HKEY_CLASSES_ROOT\\%s", FileTypes[i].Name); base.DeleteKey(); } } }; DefaultClient::FileType DefaultClient::FileTypes[] = { { "Scribe.Email", "Email", 2 }, { "Scribe.Folder", "Mailbox", 0 }, { "Scribe.Calendar", "Calendar Event", 6 }, { "Scribe.Contact", "Contact", 4 }, { "Scribe.MailStore", "Mail Store", 0 }, { "Scribe.Mailto", "Mailto Protocol", 0 }, { 0, 0 } }; #endif void ScribeWnd::SetDefaultHandler() { #if WINNATIVE if (LAppInst->GetOption("noreg")) return; LVariant RegisterClient; if (!GetOptions()->GetValue(OPT_RegisterWindowsClient, RegisterClient)) RegisterClient = true; if (!RegisterClient.CastInt32()) return; // Create IE mail client entries for local machine and current user DefaultClient Def; bool OldAssert = LRegKey::AssertOnError; LRegKey::AssertOnError = false; bool RegistryOk = ( !Def.IsWindowsXp() || Def.InstallMailto(true) ) && Def.InstallAsClient("HKLM", true) && Def.Win7Install(true); LRegKey::AssertOnError = OldAssert; if (!RegistryOk) { // Need write permissions to fix up the registry? NeedsCapability("RegistryWritePermissions"); return; } // Check if the user wants us to be the default client LVariant n = true; GetOptions()->GetValue(OPT_CheckDefaultEmail, n); if (n.CastInt32()) { // HKEY_CURRENT_USER\Software\Microsoft\Windows\Shell\Associations\UrlAssociations\mailto\UserChoice LRegKey::AssertOnError = false; bool IsDef = Def.IsDefault(); if (!IsDef) { // Ask the user... auto Dlg = new DefaultClientDlg(this); Dlg->DoModal([this, Dlg, Def, OldAssert](auto dlg, auto id) { if (id) { auto Error = !Def.SetDefault(); LVariant v; GetOptions()->SetValue(OPT_CheckDefaultEmail, v = (int) (!Dlg->DontWarn)); OnSetDefaultHandler(Error, OldAssert); } delete dlg; }); } else OnSetDefaultHandler(false, OldAssert); } #endif } void ScribeWnd::OnSetDefaultHandler(bool Error, bool OldAssert) { #if WINDOWS LRegKey::AssertOnError = OldAssert; #endif if (Error) NeedsCapability("RegistryWritePermissions"); } void ScribeWnd::OnSelect(List *l, bool ChangeEvent) { Mail *m = (l && l->Length() == 1) ? (*l)[0]->IsMail() : 0; if (Commands) { bool NotCreated = m && !TestFlag(m->GetFlags(), MAIL_CREATED); Commands->SetCtrlEnabled(IDM_DELETE, l && l->Length() > 0); Commands->SetCtrlEnabled(IDM_DELETE_AS_SPAM, l && l->Length() > 0); Commands->SetCtrlEnabled(IDM_PRINT, l && l->Length() == 1); Commands->SetCtrlEnabled(IDM_REPLY, NotCreated); Commands->SetCtrlEnabled(IDM_REPLY_ALL, NotCreated); Commands->SetCtrlEnabled(IDM_FORWARD, m != 0); Commands->SetCtrlEnabled(IDM_BOUNCE, m != 0); } if (PreviewPanel && GetEffectiveLayoutMode() != 3) { if (!PreviewPanel->IsAttached()) { SetItemPreview(PreviewPanel); } Thing *t = (l && l->Length() == 1) ? (*l)[0] : 0; PreviewPanel->OnThing(t, ChangeEvent); /* if (d->Debug) d->Debug->OnThing(t); */ } } class SpellErrorInst { public: int Id; LString Word; LString::Array Suggestions; SpellErrorInst(int id) { Id = id; // Decor = LCss::TextDecorSquiggle; // DecorColour.Rgb(255, 0, 0); } ~SpellErrorInst() { } bool OnMenu(LSubMenu *m) { if (Suggestions.Length()) { for (unsigned i=0; iAppendItem(Suggestions[i], 100 + i, true); } m->AppendSeparator(); } char Buf[256]; sprintf_s(Buf, sizeof(Buf), LLoadString(IDS_ADD_TO_DICTIONARY, "Add '%s' to dictionary"), Word.Get()); m->AppendItem(Buf, 1, true); return true; } void OnMenuClick(int i) { if (i == 1) { // Add to dictionary... /* if (PostThreadEvent(SpellHnd, M_ADD_WORD, (LMessage::Param) new LString(Word))) { // FIXME LAssert(!"Impl me."); // View->PostEvent(M_DELETE_STYLE, (LMessage::Param) dynamic_cast(this)); } */ } else if (i >= 100 && i < 100 + (int)Suggestions.Length()) { // Change spelling.. char *Replace = Suggestions[i - 100]; if (Replace) { char16 *w = Utf8ToWide(Replace); if (w) { /* int NewLen = StrlenW(w); if (NewLen > Len) { // Bigger... memcpy(View->NameW() + Start, w, Len * sizeof(char16)); View->Insert(Start + Len, w + Len, NewLen - Len); } else if (NewLen < Len) { // Smaller... memcpy(View->NameW() + Start, w, NewLen * sizeof(char16)); View->Delete(Start + NewLen, Len - NewLen); } else { // Just copy... memcpy(View->NameW() + Start, w, Len * sizeof(char16)); RefreshLayout(Start, Len); } */ DeleteArray(w); } } } } }; class MailTextView : public LTextView3 { ScribeWnd *App; LSpellCheck *Thread; LColour c[8]; LHashTbl, SpellErrorInst*> ErrMap; SpellErrorInst *NewErrorInst() { int Id; while (ErrMap.Find(Id = LRand(10000))) ; SpellErrorInst *Inst = new SpellErrorInst(Id); if (!Inst) return NULL; ErrMap.Add(Id, Inst); return Inst; } public: MailTextView(ScribeWnd *app, int Id, int x, int y, int cx, int cy, LFontType *FontType) : LTextView3(Id, x, y, cx, cy, FontType) { App = app; Thread = 0; int i=0; c[i++].Rgb(0x80, 0, 0); c[i++].Rgb(0, 0x80, 0); c[i++].Rgb(0, 0, 0x80); c[i++].Rgb(0x80, 0x80, 0); c[i++].Rgb(0x80, 0, 0x80); c[i++].Rgb(0, 0x80, 0x80); c[i++].Rgb(0x80, 0x80, 0x80); c[i++].Rgb(0xc0, 0xc0, 0xc0); for (i=0; i 0 && !StrchrW(SpellDelim, Text[Start-1])) Start--; if (Len > 0) { // Text being added Len += Origin - Start; while ((ssize_t)Start + Len < Size && !StrchrW(SpellDelim, Text[Start + Len])) Len++; } else if (Len < 0) { // Text being deleted Len = Origin - Start; while ((ssize_t)Start + Len < Size && !StrchrW(SpellDelim, Text[Start + Len])) Len++; } if (!Thread) Thread = App->GetSpellThread(); if (Thread && Len > 0) { LString Str(Text+Start, Len); LArray Params; Thread->Check(AddDispatch(), Str, Start, Len, &Params); } // Adjust all the positions of the styles after this. for (auto s = Style.begin(); s != Style.end(); ) { if (s->Start >= Origin && s->Owner == 1) { if (Length < 0 && s->Start < Origin - Length) { // In the deleted text... Style.Delete(s); continue; } // After the deleted text s->Start += Length; LAssert(s->Start >= 0); } s++; } } } void PourText(size_t Start, ssize_t Len) { LTextView3::PourText(Start, Len); for (auto l: Line) { int n=0; char16 *t = Text + l->Start; char16 *e = t + l->Len; while ((*t == ' ' || *t == '>') && t < e) if (*t++ == '>') n++; if (n > 0) l->c = c[(n-1)%CountOf(c)]; } } LMessage::Result OnEvent(LMessage *m) { switch (m->Msg()) { case M_CHECK_TEXT: { LAutoPtr Ct((LSpellCheck::CheckText*)m->A()); if (!Ct || !Thread) break; // Clear existing spelling error styles ssize_t Start = Ct->Start; ssize_t End = Start + Ct->Len; for (auto i = Style.begin(); i != Style.end(); ) { if (i->End() < (size_t)Start || i->Start >= End) { // Outside the area we are re-styling. i++; } else { if (i->Owner == STYLE_SPELLING) { // Existing error style inside the area Style.Delete(i); } else { // Existing non-error style... i++; } } } // Insert the new styles for (auto Ct: Ct->Errors) { SpellErrorInst *ErrInst = NewErrorInst(); LAutoPtr Style(new LTextView3::LStyle(STYLE_SPELLING)); if (Style && ErrInst) { Style->View = this; Style->Start = Ct.Start; Style->Len = Ct.Len; Style->Font = GetFont(); Style->Data = ErrInst->Id; Style->DecorColour = LColour::Red; Style->Decor = LCss::TextDecorSquiggle; ErrInst->Word = LString(Text + Style->Start, Style->End()); ErrInst->Suggestions = Ct.Suggestions; InsertStyle(Style); } } // Update the screen... Invalidate(); break; } case M_DELETE_STYLE: { /* LTextView3::LStyle *s = (LTextView3::LStyle*)m->A(); if (s && Style.HasItem(s)) { Style.Delete(s); Invalidate(); } else LAssert(0); */ break; } } return LTextView3::OnEvent(m); } bool OnStyleClick(LStyle *style, LMouse *m) { switch (style->Owner) { case STYLE_URL: { if (m->Left() && m->Down() && m->Double()) { LString s(Text + style->Start, style->Len); LUri u(s); if ( (u.sProtocol && !_stricmp(u.sProtocol, "mailto")) || LIsValidEmail(s) ) { Mailto m(App, s); Mail *email = App->CreateMail(); if (email) { m.Apply(email); email->DoUI(); return true; } } else { // Web link? LExecute(s); } } break; } default: return false; } return true; } }; LDocView *ScribeWnd::CreateTextControl(int Id, const char *MimeType, bool Editor, Mail *m) { LDocView *Ctrl = 0; // Get the default font LFontType FontType; bool UseFont = FontType.Serialize(GetOptions(), OPT_EditorFont, false); if (Editor) { if (MimeType && !_stricmp(MimeType, sTextHtml)) { // Use the built in html editor LRichTextEdit *Rte; if ((Ctrl = Rte = new LRichTextEdit(Id))) { if (UseFont) Ctrl->SetFont(FontType.Create(), true); // Give the control the speller settings: LVariant Check, Lang, Dict; if (GetOptions()->GetValue(OPT_SpellCheck, Check) && Check.CastInt32() != 0) { if (GetOptions()->GetValue(OPT_SpellCheckLanguage, Lang)) Rte->SetValue(LDomPropToString(SpellCheckLanguage), Lang); if (GetOptions()->GetValue(OPT_SpellCheckDictionary, Dict)) Rte->SetValue(LDomPropToString(SpellCheckDictionary), Dict); // Set the spell thread: LSpellCheck *t = GetSpellThread(); if (t) Rte->SetSpellCheck(t); } } } else { // Use the built in plain text editor Ctrl = new MailTextView(this, Id, 0, 0, 200, 200, (UseFont) ? &FontType : 0); } } else { // Create a view only control for the mime type: LDocView *HtmlCtrl = NULL; if (!MimeType || _stricmp(MimeType, sTextPlain) == 0) Ctrl = new MailTextView(this, Id, 0, 0, 200, 200, (UseFont) ? &FontType : 0); #if 0 // defined(WINDOWS) && !defined(__GTK_H__) else if (_stricmp(MimeType, sApplicationInternetExplorer) == 0) HtmlCtrl = Ctrl = CreateIeControl(Id); #endif else HtmlCtrl = Ctrl = new Html1::LHtml(Id, 0, 0, 200, 200); if (HtmlCtrl && UseFont) { LVariant LoadImg; if (GetOptions()->GetValue(OPT_HtmlLoadImages, LoadImg)) HtmlCtrl->SetLoadImages(LoadImg.CastInt32() != 0); HtmlCtrl->SetFont(FontType.Create(), true); } } if (Ctrl) { Ctrl->SetUrlDetect(true); Ctrl->SetAutoIndent(true); LVariant WrapOption; if (GetOptions()->GetValue(OPT_WordWrap, WrapOption)) { if (WrapOption.CastInt32()) { LVariant WrapCols = 80; GetOptions()->GetValue(OPT_WrapAtColumn, WrapCols); Ctrl->SetWrapAtCol(WrapCols.CastInt32()); } else { Ctrl->SetWrapAtCol(0); } } } return Ctrl; } void ScribeWnd::GrowlInfo(LString title, LString text) { LGrowl *g = d->GetGrowl(); if (!g) return; LAutoPtr n(new LGrowl::LNotify); n->Name = "info"; n->Title = title; n->Text = text; g->Notify(n); } void ScribeWnd::GrowlOnMail(Mail *m) { LVariant v; LAutoPtr n(new LGrowl::LNotify); n->Name = "new-mail"; n->Title = m->GetSubject(); int Len = 64; char sLen[16]; sprintf_s(sLen, sizeof(sLen), "%i", Len); if (m->GetVariant("BodyAsText", v, sLen)) { char *s = v.Str(); if (s) { int Words = 0; bool Lut[256]; memset(Lut, 0, sizeof(Lut)); Lut[(int)' '] = Lut[(int)'\t'] = Lut[(int)'\r'] = Lut[(int)'\n'] = true; char *c; for (c = s; *c && Words < 30; ) { while (*c && Lut[(int)*c]) c++; while (*c && !Lut[(int)*c]) c++; Words++; } n->Text.Set(s, c - s); } } LGrowl *g = d->GetGrowl(); if (g) { g->Notify(n); m->NewEmail = Mail::NewEmailTray; } } void ScribeWnd::OnNewMailSound() { static uint64 PrevTs = 0; auto Now = LCurrentTime(); if (Now - PrevTs > 30000) { PrevTs = Now; LVariant v; if (GetOptions()->GetValue(OPT_NewMailSoundFile, v) && LFileExists(v.Str())) { LPlaySound(v.Str(), SND_ASYNC); } } } void ScribeWnd::OnFolderSelect(ScribeFolder *f) { if (SearchView) SearchView->OnFolder(); } void ScribeWnd::OnNewMail(List *MailObjs, bool Add) { if (!MailObjs) return; LVariant v; bool ShowDetail = MailObjs->Length() < 5; List NeedsFiltering; LArray NeedsBayes; LArray NeedsGrowl; LArray Resort; for (auto m: *MailObjs) { if (Add) { #if DEBUG_NEW_MAIL LgiTrace("%s:%i - NewMail.OnNewMail t=%p, uid=%s, mode=%s\n", _FL, (Thing*)m, m->GetServerUid().ToString().Get(), toString(m->NewEmail)); #endif switch (m->NewEmail) { case Mail::NewEmailNone: { auto Loaded = m->GetLoaded(); #if DEBUG_NEW_MAIL LgiTrace("%s:%i - NewMail.OnNewMail.GetLoaded=%i uid=%s\n", _FL, (int)Loaded, m->GetServerUid().ToString().Get()); #endif if (Loaded != Store3Loaded) { LOG_STORE("\tOnNewMail calling SetLoaded.\n"); m->SetLoaded(); m->NewEmail = Mail::NewEmailLoading; } else { m->NewEmail = Mail::NewEmailFilter; LOG_STORE("\tOnNewMail none->NeedsFiltering.\n"); NeedsFiltering.Insert(m); } break; } case Mail::NewEmailLoading: { auto Loaded = m->GetLoaded(); if (Loaded == Store3Loaded) { m->NewEmail = Mail::NewEmailFilter; NeedsFiltering.Insert(m); if (m->GetFolder() && !Resort.HasItem(m->GetFolder())) { Resort.Add(m->GetFolder()); } } break; } case Mail::NewEmailFilter: { NeedsFiltering.Insert(m); break; } case Mail::NewEmailBayes: { NeedsBayes.Add(m); break; } case Mail::NewEmailGrowl: { if (d->Growl) { NeedsGrowl.Add(m); break; } else { m->NewEmail = Mail::NewEmailTray; // no Growl loaded so fall through to new tray mail } } case Mail::NewEmailTray: { LAssert(m->GetObject()); Mail::NewMailLst.Insert(m); OnNewMailSound(); break; } default: { LAssert(!"Hmmm what happen?"); break; } } } else { #if DEBUG_NEW_MAIL LgiTrace("%s:%i - NewMail.OnNewMail.RemoveNewMail t=%p, uid=%s\n", _FL, (Thing*)m, m->GetServerUid().ToString().Get()); #endif Mail::NewMailLst.Delete(m); if (m->NewEmail == Mail::NewEmailFilter) m->NewEmail = Mail::NewEmailNone; } } if (Add) { // Do filtering if (NeedsFiltering.Length()) { List Filters; if (!GetOptions()->GetValue(OPT_DisableUserFilters, v) || !v.CastInt32()) { GetFilters(Filters, true, false, false); } if (Filters.Length() > 0) { // Run the filters #if DEBUG_NEW_MAIL LgiTrace("%s:%i - NewMail.OnNewMail.Filtering %i mail through %i filters\n", _FL, (int)NeedsFiltering.Length(), (int)Filters.Length()); #endif Filter::ApplyFilters(NULL, Filters, NeedsFiltering); // All the email not filtered now needs to be sent to the bayes filter. for (auto m: NeedsFiltering) { if (m->NewEmail == Mail::NewEmailBayes) { #if DEBUG_NEW_MAIL LgiTrace("%s:%i - NewMail.OnNewMail.NeedsBayes t=%p, msgid=%s\n", _FL, (Thing*)m, m->GetMessageId()); #endif NeedsBayes.Add(m); } else if (m->NewEmail == Mail::NewEmailGrowl) { #if DEBUG_NEW_MAIL LgiTrace("%s:%i - NewMail.OnNewMail.NeedsGrowl t=%p, msgid=%s\n", _FL, (Thing*)m, m->GetMessageId()); #endif NeedsGrowl.Add(m); } } } } // Do bayes if (NeedsBayes.Length()) { ScribeBayesianFilterMode FilterMode = BayesOff; if (GetOptions()->GetValue(OPT_BayesFilterMode, v)) FilterMode = (ScribeBayesianFilterMode)v.CastInt32(); for (unsigned i=0; iMailMessageIdMap(); // Start the Bayesian rating process off Store3Status Status = IsSpam(Rating, m); if (Status == Store3Success) { // Bayes done... this stops OnBayesResult from passing it back to OnNewMail m->NewEmail = Mail::NewEmailGrowl; // Process bayes result if (!OnBayesResult(m, Rating)) { // Not spam... so on to growl #if DEBUG_NEW_MAIL LgiTrace("%s:%i - NewMail.Bayes.NeedsGrowl t=%p, msgid=%s\n", _FL, (Thing*)m, m->GetMessageId()); #endif NeedsGrowl.Add(m); } else { // Is spam... do nothing... #if DEBUG_NEW_MAIL LgiTrace("%s:%i - NEW_MAIL: Bayes->IsSpam t=%p, msgid=%s\n", _FL, (Thing*)m, m->GetMessageId()); #endif m->NewEmail = Mail::NewEmailNone; m = 0; } } else { // Didn't get classified immediately, so it'll be further // processed when OnBayesResult gets called later. } } else { // Bayes filter not active... move it to growl m->NewEmail = Mail::NewEmailGrowl; NeedsGrowl.Add(m); } } } if (NeedsGrowl.Length()) { if (d->Growl) { if (!ShowDetail) { LAutoPtr n(new LGrowl::LNotify); n->Name = "new-mail"; n->Title = "New Mail"; n->Text.Printf("%i new messages", (int)MailObjs->Length()); d->Growl->Notify(n); } else { for (unsigned i=0; iGetLoaded(); LAssert(state == Store3Loaded); // If loaded then notify GrowlOnMail(m); } } } for (unsigned i=0; iNewEmail = Mail::NewEmailTray; #if DEBUG_NEW_MAIL LgiTrace("%s:%i - NewMail.OnNewMail.Growl->Tray t=%p, msgid=%s\n", _FL, (Thing*)m, m->GetMessageId()); #endif LAssert(m->GetObject()); Mail::NewMailLst.Insert(m); OnNewMailSound(); } } if (GetOptions()->GetValue(OPT_NewMailNotify, v) && v.CastInt32()) { PostEvent(M_SCRIBE_NEW_MAIL); } for (unsigned i=0; iGetPath(); LgiTrace("%s:%i - NewMail.OnNewMail.Resort=%s\n", _FL, Path.Get()); #endif Resort[i]->ReSort(); } } } LColour ScribeWnd::GetColour(int i) { static LColour MailPreview; static LColour UnreadCount; #define ReadColDef(Var, Tag, Default) \ case Tag: \ { \ if (!Var.IsValid()) \ { \ Var = Default; \ LColour::GetConfigColour("Colour."#Tag, Var); \ } \ return Var; \ break; \ } switch (i) { ReadColDef(MailPreview, L_MAIL_PREVIEW, LColour(0, 0, 255)); ReadColDef(UnreadCount, L_UNREAD_COUNT, LColour(0, 0, 255)); default: { return LColour((LSystemColour)i); break; } } return LColour(); } bool WriteXmlTag(LStream &p, LXmlTag *t) { const char *Tag = t->GetTag(); bool ValidTag = ValidStr(Tag) && !IsDigit(Tag[0]); if (ValidTag) p.Print("<%s", Tag); else { LAssert(0); return false; } LXmlTree Tree; static const char *EncodeEntitiesAttr = "\'<>\"\n"; for (unsigned i=0; iAttr.Length(); i++) { auto &a = t->Attr[i]; // Write the attribute name p.Print(" %s=\"", a.GetName()); // Encode the value if (!Tree.EncodeEntities(&p, a.GetValue(), -1, EncodeEntitiesAttr)) { LAssert(0); return false; } // Write the delimiter p.Write((void*)"\"", 1); if (iAttr.Length()-1 /*&& TestFlag(d->Flags, GXT_PRETTY_WHITESPACE)*/) { p.Write((void*)"\n", 1); } } p.Write(">", 1); return true; } LString ScribeWnd::ProcessReplyForwardTemplate(Mail *m, Mail *r, char *Xml, int &Cursor, const char *MimeType) { LStringPipe p(256); if (m && r && Xml) { bool IsHtml = MimeType && !_stricmp(MimeType, sTextHtml); LMemStream mem(Xml, strlen(Xml)); LXmlTag x; LXmlTree t(GXT_KEEP_WHITESPACE | GXT_NO_DOM); if (t.Read(&x, &mem, 0)) { ScribeDom Dom(this); Dom.Email = m; if (IsHtml) { const char *EncodeEntitiesContent = "\'<>\""; for (auto Tag: x.Children) { if (!WriteXmlTag(p, Tag)) { break; } for (const char *c = Tag->GetContent(); c; ) { const char *s = strstr(c, "") : NULL; if (s && e) { if (s > c) { t.EncodeEntities(&p, (char*)c, s - c, EncodeEntitiesContent); } s += 2; LString Var = LString(s, e - s).Strip(); LVariant v; if (Var) { LString::Array parts = Var.SplitDelimit(" "); if (parts.Length() > 0) { if (Dom.GetValue(parts[0], v)) { for (unsigned mod = 1; mod < parts.Length(); mod++) { LString::Array m = parts[mod].SplitDelimit("=", 1); if (m.Length() == 2) { if (m[0].Equals("quote")) { LVariant Quote; if (Dom.GetValue(m[1], Quote)) { LVariant WrapColumn; if (!GetOptions()->GetValue(OPT_WrapAtColumn, WrapColumn) || WrapColumn.CastInt32() <= 0) WrapColumn = 76; WrapAndQuote(p, Quote.Str(), WrapColumn.CastInt32(), v.Str(), NULL, MimeType); v.Empty(); } } } } switch (v.Type) { case GV_STRING: { p.Push(v.Str()); break; } case GV_DATETIME: { p.Push(v.Value.Date->Get()); break; } case GV_NULL: break; default: { LAssert(!"Unsupported type."); break; } } } } } c = e + 2; } else { p.Print("%s", c); break; } } } } else { LArray Tags; Tags.Add(&x); for (auto Tag: x.Children) { Tags.Add(Tag); } for (unsigned i=0; iGetTag() && Dom.GetValue(Tag->GetTag(), v)) { char *s = v.Str(); if (s) { const char *Quote; if ((Quote = Tag->GetAttr("quote"))) { LVariant q, IsQuote; GetOptions()->GetValue(OPT_QuoteReply, IsQuote); if (r->GetValue(Quote, q)) { Quote = q.Str(); } else { Quote = "> "; } if (Quote && IsQuote.CastInt32()) { LVariant WrapColumn; if (!GetOptions()->GetValue(OPT_WrapAtColumn, WrapColumn) || WrapColumn.CastInt32() <= 0) WrapColumn = 76; WrapAndQuote(p, Quote, WrapColumn.CastInt32(), s); } else { p.Push(s); } } else { p.Push(s); } } else if (v.Type == GV_DATETIME && v.Value.Date) { char b[64]; v.Value.Date->Get(b, sizeof(b)); p.Push(b); } } else if (Tag->IsTag("cursor")) { int Size = (int)p.GetSize(); char *Buf = new char[Size+1]; if (Buf) { p.Peek((uchar*)Buf, Size); Buf[Size] = 0; RemoveReturns(Buf); Cursor = LCharLen(Buf, "utf-8"); DeleteArray(Buf); } } if (Tag->GetContent()) { p.Push(Tag->GetContent()); } } } } } return p.NewLStr(); } LAutoString ScribeWnd::ProcessSig(Mail *m, char *Xml, const char *MimeType) { LStringPipe p; if (!m || !Xml) return LAutoString(); if (MimeType && !_stricmp(MimeType, sTextHtml)) p.Write(Xml, strlen(Xml)); else { LMemStream mem(Xml, strlen(Xml)); LXmlTag x; LXmlTree t(GXT_KEEP_WHITESPACE|GXT_NO_DOM); if (t.Read(&x, &mem, 0)) { for (auto Tag: x.Children) { if (Tag->IsTag("random-line")) { char *FileName = 0; if ((FileName = Tag->GetAttr("Filename"))) { - char *File = LReadTextFile(FileName); - if (File) + LFile f(FileName); + if (f) { - LToken Lines(File, "\r\n"); - DeleteArray(File); + auto Lines = f.Read().SplitDelimit("\r\n"); char *RandomLine = Lines[LRand((unsigned)Lines.Length())]; if (RandomLine) { p.Push(RandomLine); } } } } else if (Tag->IsTag("random-paragraph")) { char *FileName = 0; if ((FileName = Tag->GetAttr("Filename"))) { char *File = LReadTextFile(FileName); if (File) { List Para; for (char *f=File; f && *f; ) { // skip whitespace while (strchr(" \t\r\n", *f)) f++; if (*f) { char *Start = f; char *n; while ((n = strchr(f, '\n'))) { f = n + 1; if (f[1] == '\n' || (f[1] == '\r' && f[2] == '\n')) { break; } } if (f == Start) f += strlen(f); Para.Insert(NewStr(Start, f-Start)); } } DeleteArray(File); char *RandomPara = Para.ItemAt(LRand((int)Para.Length())); if (RandomPara) { p.Push(RandomPara); } Para.DeleteArrays(); } } } else if (Tag->IsTag("include-file")) { char *FileName = 0; if ((FileName = Tag->GetAttr("filename"))) { char *File = LReadTextFile(FileName); if (File) { p.Push(File); DeleteArray(File); } } } else if (Tag->IsTag("quote-file")) { char *FileName = 0; char *QuoteStr = 0; if ((FileName = Tag->GetAttr("filename")) && (QuoteStr = Tag->GetAttr("Quote"))) { } } else { p.Push(Tag->GetContent()); } } } } return LAutoString(p.NewStr()); } // Get the effective permissions for a resource. // // This method can be used by both sync and async code: // In sync mode, don't supply a callback (ie = NULL) and the return value will be: // Store3Error - no access // Store3Delayed - no access, asking the user for password // Store3Success - allow immediate access // // In async mode, supply a callback and wait for the response. // callback(false) - no access // callback(true) - allow immediate access // in this mode the same return values as sync mode are used. Store3Status ScribeWnd::GetAccessLevel(LViewI *Parent, ScribePerm Required, const char *ResourceName, std::function Callback) { if (Required >= CurrentAuthLevel) { if (Callback) Callback(true); return Store3Success; } if (!Parent) Parent = this; switch (Required) { default: break; case PermRequireUser: { GPassword p; if (!p.Serialize(GetOptions(), OPT_UserPermPassword, false)) { if (Callback) Callback(true); return Store3Success; } char Msg[256]; sprintf_s(Msg, sizeof(Msg), LLoadString(IDS_ASK_USER_PASS), ResourceName); auto d = new LInput(Parent, "", Msg, AppName, true); d->DoModal([this, d, p, Callback](auto dlg, auto id) { if (id && d->GetStr()) { char Pass[256]; p.Get(Pass); bool Status = strcmp(Pass, d->GetStr()) == 0; if (Status) { CurrentAuthLevel = PermRequireUser; auto i = Menu->FindItem(IDM_LOGOUT); if (i) i->Enabled(true); if (Callback) Callback(true); } else { if (Callback) Callback(false); } } delete dlg; }); return Store3Delayed; } case PermRequireAdmin: { LString Key; Key.Printf("Scribe.%s", OPT_AdminPassword); auto Hash = LAppInst->GetConfig(Key); if (ValidStr(Hash)) { if (Callback) Callback(false); return Store3Error; } uchar Bin[256]; ssize_t BinLen = 0; if ((BinLen = ConvertBase64ToBinary(Bin, sizeof(Bin), Hash, strlen(Hash))) != 16) { LgiMsg(Parent, "Admin password not correctly encoded.", AppName); if (Callback) Callback(false); return Store3Error; } auto d = new LInput(Parent, "", LLoadString(IDS_ASK_ADMIN_PASS), AppName, true); d->DoModal([this, d, Bin, Callback](auto dlg, auto id) { if (id && d->GetStr()) { unsigned char Digest[16]; char Str[256]; sprintf_s(Str, sizeof(Str), "%s admin", d->GetStr().Get()); MDStringToDigest(Digest, Str); if (memcmp(Bin, Digest, 16) == 0) { CurrentAuthLevel = PermRequireAdmin; auto i = Menu->FindItem(IDM_LOGOUT); if (i) i->Enabled(true); if (Callback) Callback(true); } else { if (Callback) Callback(false); } } delete dlg; }); return Store3Delayed; } } if (Callback) Callback(true); return Store3Success; } void ScribeWnd::GetAccountSettingsAccess(LViewI *Parent, ScribeAccessType AccessType, std::function Callback) { LVariant Level = (int)PermRequireNone; // Check if user level access is required char *Opt = (char*)(AccessType == ScribeReadAccess ? OPT_AccPermRead : OPT_AccPermWrite); GetOptions()->GetValue(Opt, Level); /* // Check if admin access is required char *Admin = GetScribeAccountPerm((char*) (AccessType == ScribeReadAccess ? "Read" : "Write")); if (Admin && _stricmp(Admin, "Admin") == 0) { Level = PermRequireAdmin; } */ GetAccessLevel(Parent ? Parent : this, (ScribePerm)Level.CastInt32(), "Account Settings", Callback); } LMutex *ScribeWnd::GetLock() { return _Lock; } void ScribeWnd::OnBeforeConnect(ScribeAccount *Account, bool Receive) { if (Receive) { Account->Receive.Enabled(false); } else { Account->Send.Enabled(true); } if (StatusPanel) { StatusPanel->Invalidate(); } } void ScribeWnd::OnAfterConnect(ScribeAccount *Account, bool Receive) { if (Account) { SaveOptions(); if (ScribeState == ScribeExiting) { LCloseApp(); } } if (Receive) { Account->Receive.Enabled(true); } else { Account->Send.Enabled(true); } if (StatusPanel) { StatusPanel->Invalidate(); } if (d->SendAfterReceive) { bool Online = false; for (auto a: Accounts) { bool p = a->Receive.IsPersistant(); bool o = a->Receive.IsOnline(); if (!p && o) { Online = true; break; } } if (!Online) { Send(-1, true); d->SendAfterReceive = false; } } } void ScribeWnd::Send(int Which, bool Quiet) { if (ScribeState == ScribeExiting) return; if (Which < 0) { LVariant v; if (GetOptions()->GetValue(OPT_DefaultSendAccount, v)) Which = v.CastInt32(); } LArray Outboxes; unsigned i; for (i=0; iLoadThings(); Outboxes.Add(OutBox); } } if (Outboxes.Length() < 1) { LgiMsg(this, LLoadString(IDS_NO_OUTGOING_FOLDER), AppName, MB_OK); return; } int MailToSend = 0; List Acc; SendAccountlet *Default = 0; { // Create list of accounts for (auto a: Accounts) { Acc.Insert(&a->Send); a->Send.Outbox.DeleteObjects(); if (Which < 0 || a->GetIndex() == Which) Default = &a->Send; } // If the default it not in the list try the first one.. if (!Default) Default = Acc[0]; } for (i=0; iItems) { Mail *m = t->IsMail(); if (!m) continue; uint32_t Flags = m->GetFlags(); if (!TestFlag(Flags, MAIL_SENT) && TestFlag(Flags, MAIL_READY_TO_SEND)) { LDataIt To = m->GetObject()->GetList(FIELD_TO); if (To && To->Length()) { LAutoPtr Out(new ScribeEnvelope); if (Out) { if (m->OnBeforeSend(Out)) { SendAccountlet *Send = 0; for (auto a: Acc) { LVariant Ie = a->GetAccount()->Identity.Email(); if (ValidStr(Ie.Str()) && a->OnlySendThroughThisAccount()) { if (Ie.Str() && m->GetFromStr(FIELD_EMAIL) && _stricmp(Ie.Str(), m->GetFromStr(FIELD_EMAIL)) == 0) { Send = a; break; } } } if (!Send) { Send = Default; } if (Send) { LAssert(Out->To.Length() > 0); Out->SourceFolder = OutBox->GetPath(); Send->Outbox.Add(Out.Release()); MailToSend++; } } } } else { LgiMsg( this, LLoadString(IDS_ERROR_NO_RECIPIENTS), AppName, MB_OK, m->GetSubject() ? m->GetSubject() : (char*)"(none)"); } } } } if (MailToSend) { for (auto a: Acc) { if (a->Outbox.Length() > 0 && !a->IsOnline()) { if (a->IsConfigured()) { a->Connect(0, Quiet); } else { auto d = new LAlert(this, AppName, LLoadString(IDS_ERROR_NO_CONFIG_SEND), LLoadString(IDS_CONFIGURE), LLoadString(IDS_CANCEL)); d->DoModal([this, d, a](auto dlg, auto id) { if (id == 1) a->GetAccount()->InitUI(this, 1, NULL); delete dlg; }); } } } } else { LgiMsg(this, LLoadString(IDS_NO_MAIL_TO_SEND), AppName, MB_OK, Outboxes[0]->GetText()); } } void ScribeWnd::Receive(int Which) { #define LOG_RECEIVE 0 if (ScribeState == ScribeExiting) { LgiTrace("%s:%i - Won't receive, is trying to exit.\n", _FL); return; } for (ScribeAccount *i: Accounts) { if (i->GetIndex() != Which) continue; if (i->Receive.IsOnline()) { #if LOG_RECEIVE LgiTrace("%s:%i - %i already online.\n", _FL, Which); #endif } else if (i->Receive.Disabled() > 0) { #if LOG_RECEIVE LgiTrace("%s:%i - %i is disabled.\n", _FL, Which); #endif } else if (!i->Receive.IsConfigured()) { #if LOG_RECEIVE LgiTrace("%s:%i - %i is not configured.\n", _FL, Which); #endif auto a = new LAlert(this, AppName, LLoadString(IDS_ERROR_NO_CONFIG_RECEIVE), LLoadString(IDS_CONFIGURE), LLoadString(IDS_CANCEL)); a->DoModal([this, a, i](auto dlg, auto id) { if (id == 1) i->InitUI(this, 2, NULL); delete dlg; }); } else { i->Receive.Connect(0, false); } break; } } bool ScribeWnd::GetHelpFilesPath(char *Path, int PathSize) { const char *Index = "index.html"; char Install[MAX_PATH_LEN]; strcpy_s(Install, sizeof(Install), ScribeResourcePath()); for (int i=0; i<5; i++) { char p[MAX_PATH_LEN]; LMakePath(p, sizeof(p), Install, "Help"); LMakePath(p, sizeof(p), p, Index); LgiTrace("Trying '%s'\n", p); if (LFileExists(p)) { LTrimDir(p); strcpy_s(Path, PathSize, p); return true; } #ifdef MAC LMakePath(p, sizeof(p), Install, "Resources/Help"); LMakePath(p, sizeof(p), p, Index); // LgiTrace("Trying '%s'\n", p); if (LFileExists(p)) { LTrimDir(p); strcpy_s(Path, PathSize, p); return true; } #endif LTrimDir(Install); // Try all the parent folders... } LArray Ext; LArray Help; Ext.Add("index.html"); LRecursiveFileSearch(Install, &Ext, &Help); for (unsigned i=0; iSetEvents(d); return Browse->SetUri(Path); } #else #ifdef MAC if (LExecute(Path)) return true; #else if (Hash) *Hash = '#'; // Get browser... char Browser[256]; if (!LGetAppForMimeType("application/browser", Browser, sizeof(Browser))) { LgiTrace("%s:%i - LGetAppForMimeType('text/html') failed.\n", _FL); goto HelpError; } // Execute browser to view help... char Uri[256]; sprintf_s(Uri, sizeof(Uri), "\"file://%s\"", Path); #ifdef WIN32 char *c; while (c = strchr(Uri, '\\')) *c = '/'; #endif LgiTrace("LaunchHelp('%s','%s').\n", Browser, Uri); if (!LExecute(Browser, Uri)) { LgiTrace("%s:%i - LExecute('%s','%s') failed.\n", _FL, Browser, Uri); goto HelpError; } return true; #endif #endif HelpError: LgiMsg(this, LLoadString(IDS_ERROR_NO_HELP), AppName, MB_OK); } return false; } void ScribeWnd::Preview(int Which) { LArray a; a.Add(Accounts[Which]); OpenPopView(this, a); } bool MergeSegments(LDataPropI *DstProp, LDataPropI *SrcProp, LDom *Dom) { LDataI *Src = dynamic_cast(SrcProp); LDataI *Dst = dynamic_cast(DstProp); if (!Dst || !Src) return false; Store3MimeType Mt(Src->GetStr(FIELD_MIME_TYPE)); if (Mt.IsText()) { // Set the headers... Dst->SetStr(FIELD_INTERNET_HEADER, Src->GetStr(FIELD_INTERNET_HEADER)); // Do mail merge of data part... LAutoStreamI Data = Src->GetStream(_FL); if (Data) { // Read data out into a string string... LAutoString Str(new char[(int)Data->GetSize()+1]); Data->Read(Str, (int)Data->GetSize()); Str[Data->GetSize()] = 0; // Do field insert and save result to segment char *Merged = ScribeInsertFields(Str, Dom); LAutoStreamI Mem(new LMemStream(Merged, strlen(Merged))); Dst->SetStream(Mem); } } else { // Straight copy... Dst->CopyProps(*Src); } // Merge children segments as well LDataIt Sc = Src->GetList(FIELD_MIME_SEG); LDataIt Dc = Dst->GetList(FIELD_MIME_SEG); if (Dc && Sc) { for (unsigned i=0; iLength(); i++) { // Create new dest child, and merge the source child across LDataPropI *NewDestChild = Dc->Create(Dst->GetStore()); if (!MergeSegments(NewDestChild, (*Sc)[i], Dom)) return false; LDataI *NewDest = dynamic_cast(NewDestChild); if (NewDest) NewDest->Save(Dst); } } return true; } // Either 'FileName' or 'Source' will be valid void ScribeWnd::MailMerge(LArray &Contacts, const char *FileName, Mail *Source) { ScribeFolder *Outbox = GetFolder(FOLDER_OUTBOX); if (Outbox && Contacts.Length() && (FileName || Source)) { LAutoPtr ImportEmail; if (FileName) { LAutoPtr File(new LFile); if (File->Open(FileName, O_READ)) { Thing *t = CreateItem(MAGIC_MAIL, Outbox, false); ImportEmail.Reset(Source = t->IsMail()); if (!Source->Import(Source->AutoCast(File), sMimeMessage)) { Source = 0; } } } if (Source) { List Msgs; ScribeDom Dom(this); // Do the merging of the document with the database for (unsigned i=0; iGetContact() : 0; Contact *temp = new Contact(this); if (!c) { temp->SetFirst(la->sName); temp->SetEmail(la->sAddr); c = temp; } Dom.Con = c; Thing *t = CreateItem(MAGIC_MAIL, Outbox, false); if (t) { Dom.Email = t->IsMail(); LAutoString s; if (s.Reset(ScribeInsertFields(Source->GetSubject(), &Dom))) Dom.Email->SetSubject(s); LDataPropI *Recip = Dom.Email->GetTo()->Create(Dom.Email->GetObject()->GetStore()); if (Recip) { Recip->CopyProps(*Dom.Con->GetObject()); Recip->SetInt(FIELD_CC, 0); Dom.Email->GetTo()->Insert(Recip); } MergeSegments( Dom.Email->GetObject()->GetObj(FIELD_MIME_SEG), Source->GetObject()->GetObj(FIELD_MIME_SEG), &Dom); Msgs.Insert(Dom.Email); } temp->DecRef(); } // Ask user what to do if (Msgs[0]) { char Msg[256]; sprintf_s(Msg, sizeof(Msg), LLoadString(IDS_MAIL_MERGE_Q), Msgs.Length()); auto Ask = new LAlert(this, AppName, Msg, LLoadString(IDS_SAVE_TO_OUTBOX), LLoadString(IDS_CANCEL)); Ask->DoModal([this, Msgs, Outbox](auto dlg, auto id) { switch (id) { case 1: // Save To Outbox { for (size_t i=0; iSave(Outbox); } break; } case 2: // Cancel { for (size_t i=0; iOnDelete(); } break; } } delete dlg; }); } else { LgiMsg(this, LLoadString(IDS_MAIL_MERGE_EMPTY), AppName); } } } } ScribeFolder *CastFolder(LDataI *f) { if (!f) { LAssert(!"Null pointer"); return 0; } if (f->Type() == MAGIC_FOLDER) { return (ScribeFolder*)f->UserData; } return 0; } Thing *CastThing(LDataI *t) { if (!t) { LAssert(!"Null pointer"); return 0; } if (t->Type() == MAGIC_FOLDER) { LAssert(!"Shouldn't be a folder"); return 0; } return (Thing*)t->UserData; } LString _GetUids(LArray &items) { LString::Array a; for (auto i: items) a.New().Printf(LPrintfInt64, i->GetInt(FIELD_SERVER_UID)); return LString(",").Join(a); } /// Received new items from a storage backend. void ScribeWnd::OnNew( /// The parent folder of the new item LDataFolderI *Parent, /// All the new items LArray &NewItems, /// The position in the parent folder or -1 int Pos, /// Non-zero if the object is a new email. bool IsNew) { ScribeFolder *Fld = CastFolder(Parent); int UnreadDiff = 0; if (Stricmp(Parent->GetStr(FIELD_FOLDER_NAME), "Contacts") && Stricmp(Parent->GetStr(FIELD_FOLDER_NAME), "Calendar")) { LOG_STORE("OnNew(%s, %s, %i, %i)\n", Parent->GetStr(FIELD_FOLDER_NAME), _GetUids(NewItems).Get(), Pos, IsNew); } if (!Fld) { // When this happens the parent hasn't been loaded by the UI thread // yet, so if say the IMAP back end notices a new sub-folder then we // can safely ignore it until such time as the UI loads the parent // folder. At which point the child we got told about here will be // loaded anyway. #if DEBUG_NEW_MAIL LDataFolderI *p = dynamic_cast(Parent); LgiTrace("%s:%i - NewMail.OnNew no UI object for '%s'\n", _FL, p ? p->GetStr(FIELD_FOLDER_NAME) : NULL); #endif return; } if (!Parent || !NewItems.Length()) { LAssert(!"Param error"); return; } for (auto cb: d->Store3EventCallbacks) cb->OnNew(Parent, NewItems, Pos, IsNew); List NewMail; for (auto Item: NewItems) { if (Item->Type() == MAGIC_FOLDER) { // Insert new folder into the right point on the tree... LDataFolderI *SubObject = dynamic_cast(Item); if (!SubObject) { LAssert(!"Not a valid folder."); continue; } ScribeFolder *Sub = CastFolder(SubObject); // LgiTrace("OnNew '%s', Sub=%p Children=%i\n", SubObject->GetStr(FIELD_FOLDER_NAME), Sub, SubObject->SubFolders().Length()); if (!Sub) { // New folder... if ((Sub = new ScribeFolder)) { Sub->App = this; Sub->SetObject(SubObject, false, _FL); Fld->Insert(Sub); } } else { // Existing folder... Fld->Insert(Sub, Pos); } } else { Thing *t = CastThing(Item); if (!t) { // Completely new thing... t = CreateThingOfType((Store3ItemTypes) Item->Type(), Item); } if (t) { if (t->DeleteOnAdd.Obj) { // Complete a delayed move auto OldFolder = t->App->GetFolder(t->DeleteOnAdd.Path); if (!OldFolder) { LgiTrace("%s:%i - Couldn't resolve old folder '%s'\n", _FL, t->DeleteOnAdd.Path.Get()); } else { auto OldItem = t->DeleteOnAdd.Obj; if (!OldFolder->Items.HasItem(OldItem)) LgiTrace("%s:%i - Couldn't find old obj.\n", _FL); else OldItem->OnDelete(); } } t->SetParentFolder(Fld); Fld->Update(); if (Fld->Select()) { // Existing thing... t->SetFieldArray(Fld->GetFieldArray()); ThingFilter *Filter = GetThingFilter(); if (!Filter || Filter->TestThing(t)) { MailList->Insert(t, -1, false); } } Mail *m = t->IsMail(); if (m) { // LgiTrace("OnNew %p\n", m->GetObject()); UnreadDiff += TestFlag(m->GetFlags(), MAIL_READ) ? 0 : 1; #if DEBUG_NEW_MAIL LgiTrace("%s:%i - NewMail.OnNew t=%p uid=%s IsNew=%i\n", _FL, t, m->GetServerUid().ToString().Get(), IsNew); #endif if (IsNew) { LAssert(!NewMail.HasItem(m)); NewMail.Insert(m); } } t->Update(); } } } if (UnreadDiff) Fld->OnUpdateUnRead(UnreadDiff, false); if (MailList && Fld->Select()) MailList->Sort(ListItemCompare, (NativeInt)Fld); if (NewMail.Length()) OnNewMail(&NewMail); } void ScribeWnd::OnPropChange(LDataStoreI *store, int Prop, LVariantType Type) { switch (Prop) { case FIELD_IS_ONLINE: { // This is in case we receive a message after the app has shutdown and // deleted 'this'. if (ScribeState > ScribeRunning) break; if (StatusPanel) StatusPanel->Invalidate(); for (auto a : Accounts) { if (a->Receive.GetDataStore() == store) { int64 Online = store->GetInt(FIELD_IS_ONLINE); auto Old = ScribeState; // This prevents the folders unloading during this call. // Which causes crashes. ScribeState = ScribeLoadingFolders; a->Receive.OnOnlineChange(Online != 0); ScribeState = Old; break; } } break; } } } void ScribeWnd::SetContext(const char *file, int line) { d->CtxFile = file; d->CtxLine = line; } bool ScribeWnd::OnChange(LArray &items, int FieldHint) { bool UpdateSelection = false; List NewMail; ScribeFolder *Parent = 0; // LOG_STORE("OnChange(%i, %i)\n", (int)items.Length(), FieldHint); LAssert(d->CtxFile != NULL); for (unsigned c=0; cStore3EventCallbacks.Length(); c++) { d->Store3EventCallbacks[c]->OnChange(items, FieldHint); } for (unsigned i=0; iType() == MAGIC_FOLDER) { auto ItemFolder = dynamic_cast(Item); ScribeFolder *fld = CastFolder(ItemFolder); if (fld) { if (FieldHint == FIELD_STATUS) { // This is the delayed folder load case: fld->IsLoaded(true); if (fld->Select()) fld->Populate(GetMailList()); } else { fld->Update(); } } else { // LAssert(!"Can't cast to folder?"); } } else if ((t = CastThing(Item))) { ThingUi *Ui = t->GetUI(); if (Ui) Ui->OnChange(); Mail *m = t->IsMail(); if (m) { auto StoreFlags = Item->GetInt(FIELD_FLAGS); #if 0 LgiTrace("App.OnChange(%i) handler %p: %s -> %s\n", FieldHint, m, EmailFlagsToStr(m->FlagsCache).Get(), EmailFlagsToStr(StoreFlags).Get()); #endif if (TestFlag(m->FlagsCache, MAIL_NEW) && !TestFlag(StoreFlags, MAIL_NEW)) { Mail::NewMailLst.Delete(m); Parent = m->GetFolder(); } if (m->FlagsCache != StoreFlags) { // LgiTrace("%s:%i - OnChange mail flags changed.\n", _FL); m->SetFlagsCache(StoreFlags, false, false); Parent = m->GetFolder(); } if (m->NewEmail == Mail::NewEmailLoading) { auto Loaded = m->GetLoaded(); if (Loaded < Store3Loaded) { #if DEBUG_NEW_MAIL LgiTrace("%s:%i - NewMail.OnChange.GetBody t=%p, uid=%s, mode=%s, loaded=%s (%s:%i)\n", _FL, (Thing*)m, m->GetServerUid().ToString().Get(), toString(m->NewEmail), toString(Loaded), d->CtxFile, d->CtxLine); #endif m->GetBody(); } else { #if DEBUG_NEW_MAIL LgiTrace("%s:%i - NewMail.OnChange.NewMail t=%p, uid=%s, mode=%s, loaded=%s (%s:%i)\n", _FL, (Thing*)m, m->GetServerUid().ToString().Get(), toString(m->NewEmail), toString(Loaded), d->CtxFile, d->CtxLine); #endif NewMail.Insert(m); } } } else if (t->IsCalendar()) { for (auto cv: CalendarView::CalendarViews) cv->OnContentsChanged(); } if (FieldHint != FIELD_FLAGS) { // Call the on load handler... t->IsLoaded(true); } if (t->GetList()) { t->Update(); if (FieldHint != FIELD_FLAGS) UpdateSelection |= t->Select(); } } } if (MailList && UpdateSelection) { List Sel; if (MailList->GetSelection(Sel)) { OnSelect(&Sel, true); } } if (Parent) Parent->OnUpdateUnRead(0, true); if (NewMail.Length()) OnNewMail(&NewMail); d->CtxFile = NULL; d->CtxLine = 0; return true; } ContactGroup *ScribeWnd::FindGroup(char *SearchName) { ScribeFolder *g = GetFolder(FOLDER_GROUPS); if (!g || !SearchName) return 0; g->LoadThings(); for (Thing *t: g->Items) { ContactGroup *Grp = t->IsGroup(); if (!Grp) continue; auto Name = Grp->GetObject()->GetStr(FIELD_GROUP_NAME); if (Name) { if (!_stricmp(Name, SearchName)) { return Grp; } } } return 0; } bool ScribeWnd::Match(LDataStoreI *Store, LDataPropI *Address, int ObjType, LArray &Matches) { if (!Store || !Address) return 0; // char *Addr = Address->GetStr(ObjType == MAGIC_CONTACT ? FIELD_EMAIL : FIELD_NAME); auto Addr = Address->GetStr(FIELD_EMAIL); if (!Addr) return 0; List *l = GetEveryone(); if (!l) return 0; Matches.Length(0); if (ObjType == MAGIC_CONTACT) { for (auto c: *l) { if (strchr(Addr, '@')) { auto Emails = c->GetEmails(); for (auto e: Emails) { if (e.Equals(Addr)) { Matches.Add(c); break; } } } } } else if (ObjType == MAGIC_GROUP) { ScribeFolder *g = GetFolder(FOLDER_GROUPS); if (!g) return false; g->LoadThings(); for (auto t: g->Items) { ContactGroup *Grp = t->IsGroup(); if (!Grp) continue; List GrpAddr; if (!Grp->GetAddresses(GrpAddr)) continue; for (auto a: GrpAddr) { if (_stricmp(a, Addr) == 0) { Matches.Add(t); break; } } GrpAddr.DeleteArrays(); } } return Matches.Length() > 0; } bool ScribeWnd::OnMove(LDataFolderI *new_parent, LDataFolderI *old_parent, LArray &items) { if (!new_parent || items.Length() == 0) return false; bool Status = false; for (auto cb: d->Store3EventCallbacks) cb->OnMove(new_parent, old_parent, items); ScribeFolder *New = CastFolder(new_parent); ScribeFolder *Old = old_parent ? CastFolder(old_parent) : NULL; if (New) { ssize_t SelIdx = -1; for (unsigned n=0; nType()) { case MAGIC_FOLDER: { ScribeFolder *i = CastFolder(items[n]); if (i) { i->Detach(); New->Insert(i); Status = true; } break; } default: { Thing *t = CastThing(items[n]); if (t) { int UnreadMail = t->IsMail() ? !TestFlag(t->IsMail()->GetFlags(), MAIL_READ) : false; if (Old && UnreadMail) Old->OnUpdateUnRead(-1, false); if (t->GetList()) { if (t->Select() && SelIdx < 0) SelIdx = t->GetList()->IndexOf(t); t->GetList()->Remove(t); } // This closes the user interface if the object is being moved to the trash... if (New->GetSystemFolderType() == Store3SystemTrash) t->SetUI(); t->SetParentFolder(New); if (New->Select()) { MailList->Insert(t); MailList->ReSort(); } if (UnreadMail) New->OnUpdateUnRead(1, false); if (New->GetItemType() == MAGIC_ANY && t->IsMail()) { Mail::NewMailLst.Delete(t->IsMail()); } Status = true; } break; } } } if (MailList && SelIdx >= 0 && MailList->Length() > 0) { if (SelIdx >= (ssize_t)MailList->Length()) SelIdx = MailList->Length()-1; MailList->Value(SelIdx); } } return Status; } bool ScribeWnd::OnDelete(LDataFolderI *Parent, LArray &Items) { int UnreadAdjust = 0; int SelectIdx = -1; LOG_STORE("OnDelete(%s, %i)\n", Parent->GetStr(FIELD_FOLDER_NAME), (int)Items.Length()); if (!Items.Length()) return false; for (unsigned c=0; cStore3EventCallbacks.Length(); c++) { d->Store3EventCallbacks[c]->OnDelete(Parent, Items); } ScribeFolder *Fld = NULL; for (unsigned i=0; iType() == MAGIC_FOLDER) { // Insert new folder into the right point on the tree... ScribeFolder *Sub = CastFolder(Item); if (!Sub) return true; LgiTrace("OnDelete folder %p (%i)\n", (ThingType*)Sub, ThingType::DirtyThings.HasItem(Sub)); // Remove the deleted object from the dirty queue... ThingType::DirtyThings.Delete(Sub); // And make sure it can't get dirty again... Sub->SetWillDirty(false); // Remove all the children LDataIterator &SubItems = Sub->GetFldObj()->Children(); LArray Children; for (LDataI *c = SubItems.First(); c; c = SubItems.Next()) { Children.Add(c); } OnDelete(Sub->GetFldObj(), Children); // Remove the UI element Sub->Remove(); // Free the memory for the object DeleteObj(Sub); } else { Fld = Parent ? CastFolder(Parent) : 0; if (!Fld) return false; Thing *t = CastThing(Item); if (!t) { // This happens when the item moves from one store to another // of a different type and a copy of the old object is made. The // 'Thing' is detached from 'Item' and attached to the new LDataI // object. // However if the object is an unread email... we should still decrement the // folder's unread count. if (Item->Type() == MAGIC_MAIL && !(Item->GetInt(FIELD_FLAGS) & MAIL_READ)) { Fld->OnUpdateUnRead(-1, false); } } else { LAssert(!Fld || Fld == t->GetFolder()); // LgiTrace("OnDelete thing %p (%i)\n", (ThingType*)t, ThingType::DirtyThings.HasItem(t)); // Remove the deleted object from the dirty queue... ThingType::DirtyThings.Delete(t); // And make sure it can't get dirty again... t->SetWillDirty(false); // Was the thing currently being previewed? if (PreviewPanel && PreviewPanel->GetCurrent() == t) PreviewPanel->OnThing(0, false); // Was the object selected in the thing list? if (Fld && Fld->Select()) { // If so, select an adjacent item. if (SelectIdx < 0 && t->Select()) SelectIdx = MailList->IndexOf(t); MailList->Remove(t); } // Was the object an unread email? Mail *m = t->IsMail(); if (m) { if (!TestFlag(m->GetFlags(), MAIL_READ) && Fld) UnreadAdjust--; Mail::NewMailLst.Delete(m); } t->SetUI(); t->SetParentFolder(NULL); t->SetObject(NULL, false, _FL); if (Fld) Fld->Update(); if (!t->DecRef()) { t->SetDirty(false); t->SetWillDirty(false); } } } } if (Fld) Fld->OnUpdateUnRead(UnreadAdjust, false); if (SelectIdx >= 0) { LListItem *i = MailList->ItemAt(SelectIdx); if (!i && MailList->Length() > 0) i = MailList->ItemAt(MailList->Length()-1); if (i) i->Select(true); } return true; } bool ScribeWnd::AddStore3EventHandler(LDataEventsI *callback) { if (!d->Store3EventCallbacks.HasItem(callback)) { d->Store3EventCallbacks.Add(callback); } return true; } bool ScribeWnd::RemoveStore3EventHandler(LDataEventsI *callback) { d->Store3EventCallbacks.Delete(callback); return true; } bool ScribeWnd::OnMailTransferEvent(MailTransferEvent *t) { if (!Lock(_FL)) return false; LAssert(t); d->Transfers.Add(t); Unlock(); return true; } bool ScribeWnd::OnTransfer() { LVariant v; LArray Local; // Lock the transfer list if (Lock(_FL)) { // Take out a bunch of emails... for (int i=0; i<5 && d->Transfers.Length() > 0; i++) { auto t = d->Transfers[0]; LAssert(t); d->Transfers.DeleteAt(0, true); // Save them to a local array Local.Add(t); } Unlock(); if (Local.Length() == 0) // Didn't get any return false; } LArray Trans; for (auto &f: Folders) { if (f.Store) Trans.New() = f.Store->StartTransaction(); } for (auto Transfer: Local) { ReceiveStatus NewStatus = MailReceivedError; if (!Transfer) { LAssert(0); continue; } // We have to set Transfer->Status to something other than "waiting" // in all branches of this loop. Otherwise the account thread will hang. Accountlet *Acc = Transfer->Account; #if DEBUG_NEW_MAIL LgiTrace( "%s:%i - NewMail.OnTransfer t=%p receive=%i act=%i\n", _FL, Transfer, Acc->IsReceive(), Transfer->Action); #endif if (Acc->IsReceive()) { switch (Transfer->Action) { default: break; case MailDownload: case MailDownloadAndDelete: { // Import newly received mail from file ReceiveAccountlet *Receive = dynamic_cast(Acc); if (Receive) { LVariant Path = Receive->DestinationFolder(); ScribeFolder *Inbox = Path.Str() ? GetFolder(Path.Str()) : NULL; if (!Inbox) Inbox = GetFolder(FOLDER_INBOX); if (Inbox) { Mail *m = new Mail(this, Inbox->GetObject()->GetStore()->Create(MAGIC_MAIL)); if (m) { // Just in case m->SetParentFolder(Inbox); // Set the new flag... m->SetFlags(m->GetFlags() | MAIL_NEW, true, false); // Set the account to, which is used to map the email back // to the incoming account in the case that the headers // don't have the correct "To" information. m->SetAccountId(Receive->Id()); // Decode the email m->OnAfterReceive(Transfer->Rfc822Msg); LVariant v; m->SetServerUid(v = Transfer->Uid); // Save to permanent storage if (m->Save(Inbox)) { m->SetDirty(false); NewStatus = MailReceivedOk; // Only after we've safely stored the email can we // actually mark it as downloaded. if (Transfer->Uid) { Receive->AddMsg(Transfer->Uid); } } else LgiTrace("%s:%i - Error: Couldn't save mail to folders.\n", _FL); } else LgiTrace("%s:%i - Error: Memory alloc failed.\n", _FL); } else LgiTrace("%s:%i - Error: No Inbox.\n", _FL); } else LgiTrace("%s:%i - Error: Bad ptr.\n", _FL); break; } case MailHeaders: { LList *Lst; if (Transfer->Msg && (Lst = Transfer->GetList())) { //LgiTrace("Using Lst=%p\n", Lst); Lst->Insert(Transfer->Msg); NewStatus = MailReceivedOk; } break; } } } else if (Transfer->Send) { ScribeFolder *Outbox = GetFolder(Transfer->Send->SourceFolder); if (!Outbox) Outbox = GetFolder(FOLDER_OUTBOX); if (!Outbox) break; Outbox->GetMessageById(Transfer->Send->MsgId, [this, Transfer](auto m) { if (!m) { LAssert(!"Where is the email?"); LgiTrace("%s:%i - Can't find outbox for msg id '%s'\n", _FL, Transfer->Send->MsgId.Get()); } else { if (Transfer->OutgoingHeaders) { m->SetInternetHeader(Transfer->OutgoingHeaders); DeleteArray(Transfer->OutgoingHeaders); } m->OnAfterSend(); m->Save(); // Do filtering LVariant DisableFilters; GetOptions()->GetValue(OPT_DisableUserFilters, DisableFilters); if (!DisableFilters.CastInt32()) { // Run the filters List Filters; GetFilters(Filters, false, true, false); if (Filters[0]) { List In; In.Insert(m); Filter::ApplyFilters(0, Filters, In); } } // Add to bayesian spam whitelist... LVariant v; ScribeBayesianFilterMode FilterMode = BayesOff; GetOptions()->GetValue(OPT_BayesFilterMode, v); FilterMode = (ScribeBayesianFilterMode) v.CastInt32(); if (FilterMode != BayesOff && m->GetObject()) { LDataIt To = m->GetObject()->GetList(FIELD_TO); if (To) { for (LDataPropI *a = To->First(); a; a = To->Next()) { if (a->GetStr(FIELD_EMAIL)) WhiteListIncrement(a->GetStr(FIELD_EMAIL)); } } } // FIXME: // NewStatus = MailReceivedOk; } }); } #if DEBUG_NEW_MAIL LgiTrace( "%s:%i - NewMail.OnTransfer t=%p NewStatus=%i\n", _FL, Transfer, NewStatus); #endif if (NewStatus != MailReceivedOk) { // So tell the thread not to delete it from the server LgiTrace("%s:%i - Mail[%i] error: %s\n", _FL, Transfer->Index, ReceiveStatusName(Transfer->Status)); Transfer->Action = MailNoop; } Transfer->Status = NewStatus; } return Local.Length() > 0; } bool ScribeWnd::OnIdle() { bool Status = false; for (auto a : Accounts) Status |= a->Receive.OnIdle(); Status |= OnTransfer(); LMessage m(M_SCRIBE_IDLE); BayesianFilter::OnEvent(&m); SaveDirtyObjects(); #ifdef _DEBUG static uint64_t LastTs = 0; auto Now = LCurrentTime(); if (Now - LastTs >= 1000) { LastTs = Now; if (Thing::DirtyThings.Length() > 0) LgiTrace("%s:%i - Thing::DirtyThings=" LPrintfInt64 "\n", _FL, Thing::DirtyThings.Length()); } #endif return Status; } void ScribeWnd::OnScriptCompileError(const char *Source, Filter *f) { const char *CompileMsg = LLoadString(IDS_ERROR_SCRIPT_COMPILE); LArray Actions; Actions.Add(LLoadString(IDS_SHOW_CONSOLE)); Actions.Add(LLoadString(IDS_OPEN_SOURCE)); Actions.Add(LLoadString(IDS_OK)); if (!d->Bar) { // FYI Capabilities are handled in ScribeWnd::StartAction. d->ErrSource = Source; d->ErrFilter = f; d->Bar = new MissingCapsBar(this, &d->MissingCaps, CompileMsg, this, Actions); AddView(d->Bar, 2); AttachChildren(); OnPosChange(); } } diff --git a/Code/ScribeFilter.cpp b/Code/ScribeFilter.cpp --- a/Code/ScribeFilter.cpp +++ b/Code/ScribeFilter.cpp @@ -1,4212 +1,4212 @@ /* ** FILE: ScribeFilter.cpp ** AUTHOR: Matthew Allen ** DATE: 29/10/1999 ** DESCRIPTION: Scribe filters ** ** Copyright (C) 1999-2022, Matthew Allen ** fret@memecode.com ** */ #include #include #include #include #include #include "Scribe.h" #include "lgi/common/NetTools.h" #include "lgi/common/Combo.h" #include "lgi/common/Edit.h" #include "lgi/common/Button.h" #include "lgi/common/ProgressDlg.h" #include "lgi/common/FilterUi.h" #include "lgi/common/XmlTree.h" #include "lgi/common/ClipBoard.h" #include "lgi/common/TabView.h" #include "lgi/common/Printer.h" #include "lgi/common/LgiRes.h" #include "lgi/common/FileSelect.h" #include "lgi/common/Charset.h" #include "resdefs.h" #include "resource.h" #define COMBINE_OP_AND 0 #define COMBINE_OP_OR 1 #define IDM_TRUE 500 #define IDM_FALSE 501 #define IDM_NOTNEW 502 #define IDM_LOCAL 503 #define IDM_SERVER 504 #define IDM_LOCAL_AND_SERVER 505 const char *ATTR_NOT = "Not"; const char *ATTR_FIELD = "Field"; const char *ATTR_OP = "Op"; const char *ATTR_VALUE = "Value"; const char *ELEMENT_AND = "And"; const char *ELEMENT_OR = "Or"; const char *ELEMENT_CONDITION = "Condition"; const char *ELEMENT_CONDITIONS = "Conditions"; const char *ELEMENT_ACTION = "Action"; #define SkipWs(s) while ((*s) && strchr(WhiteSpace, *s)) s++; ////////////////////////////////////////////////////////////// class FilterPrivate { public: bool *Stop; LStream *Log; FilterPrivate() { Stop = 0; Log = 0; } }; ////////////////////////////////////////////////////////////// // Filter field definitions ItemFieldDef FilterFieldDefs[] = { {"Name", SdName, GV_STRING, FIELD_FILTER_NAME}, {"Index", SdIndex, GV_INT32, FIELD_FILTER_INDEX}, {"Incoming", SdIncoming, GV_INT32, FIELD_FILTER_INCOMING}, {"Outgoing", SdOutgoing, GV_INT32, FIELD_FILTER_OUTGOING}, {"Internal", SdInternal, GV_INT32, FIELD_FILTER_INTERNAL}, {0} }; int DefaultFilterFields[] = { FIELD_FILTER_NAME, FIELD_FILTER_INDEX, FIELD_FILTER_INCOMING, FIELD_FILTER_OUTGOING, FIELD_FILTER_INTERNAL, 0 }; #define ForCondField(Macro, Value) \ switch (Value) \ { \ case 0: Macro("To"); break; \ case 1: Macro("From"); break; \ case 2: Macro("Subject"); break; \ case 3: Macro("Size"); break; \ case 4: Macro("DateReceived"); break; \ case 5: Macro("DateSent"); break; \ case 6: Macro("Body"); break; \ case 7: Macro("InternetHeaders"); break; \ case 8: Macro("MessageID"); break; \ case 9: Macro("Priority"); break; \ case 10: /* flags */ break; \ case 11: Macro("Html"); break; \ case 12: Macro("Label"); break; \ case 13: Macro("From.Contact"); break; \ case 14: Macro("ImapCacheFile"); break; \ case 15: Macro("ImapFlags"); break; \ case 16: Macro("Attachments"); break; \ case 17: Macro("AttachmentNames"); break; \ case 18: Macro("From.Groups"); break; \ case 19: Macro("*"); break; \ } // Macro("", FIELD_FLAGS) struct ActionName { int Id; const char *Default; }; ActionName ActionNames[] = { {IDS_ACTION_MOVE_FOLDER, "Move to Folder"}, {IDC_DELETE, "Delete"}, {IDS_PRINT, "Print"}, {IDS_ACTION_SOUND, "Play Sound"}, {IDS_ACTION_OPEN, "Open Email"}, {IDS_ACTION_EXECUTE, "Execute Process"}, {IDS_ACTION_SET_COLOUR, "Set Colour"}, {IDS_SET_READ, "Set Read"}, {IDS_ACTION_SET_LABEL, "Set Label"}, {IDS_ACTION_EMPTY_FOLDER, "Empty Folder"}, {IDS_ACTION_MARK_SPAM, "Mark As Spam"}, {IDS_REPLY, "Reply"}, {IDS_FORWARD, "Forward"}, {IDS_BOUNCE, "Bounce"}, {IDS_ACTION_SAVE_ATTACHMENTS, "Save Attachment(s)"}, {IDS_ACTION_DELETE_ATTACHMENTS, "Delete Attachments(s)"}, {L_CHANGE_CHARSET, "Change Charset"}, {IDS_ACTION_COPY, "Copy to Folder"}, {IDS_EXPORT, "Export"}, {0, 0}, {IDS_ACTION_CREATE_FOLDER, "Create Folder"}, {0, 0} }; // These are the english names used for storing XML const char *OpNames[] = { "=", "!=", "<", "<=", ">=", ">", "Like", // LLoadString(IDS_LIKE), "Contains", // LLoadString(IDS_CONTAINS), "Starts With", // LLoadString(IDS_STARTS_WITH), "Ends With", // LLoadString(IDS_ENDS_WITH), 0 }; const char *TranslatedOpNames[] = { "=", "!=", "<", "<=", ">=", ">", 0, // like 0, // contains 0, // starts with 0, // ends with 0 }; const char **GetOpNames(bool Translated) { if (Translated) { if (TranslatedOpNames[6] == NULL) { TranslatedOpNames[6] = LLoadString(IDS_LIKE); TranslatedOpNames[7] = LLoadString(IDS_CONTAINS); TranslatedOpNames[8] = LLoadString(IDS_STARTS_WITH); TranslatedOpNames[9] = LLoadString(IDS_ENDS_WITH); } if (TranslatedOpNames[6]) { return TranslatedOpNames; } } return OpNames; } ////////////////////////////////////////////////////////////// void SkipSep(const char *&s) { while (s && *s && strchr(" \t,", *s)) s++; } LCombo *LoadTemplates(ScribeWnd *App, LView *Wnd, List &MsgIds, int Ctrl, char *Template) { LCombo *Temp; if (Wnd->GetViewById(IDC_TEMPLATE, Temp)) { ScribeFolder *Templates = App->GetFolder(FOLDER_TEMPLATES); if (Templates) { int n=0; for (auto t: Templates->Items) { Mail *m = t->IsMail(); if (m) { auto Id = m->GetMessageId(true); if (Id) { MsgIds.Insert(NewStr(Id)); Temp->Insert(m->GetSubject() ? m->GetSubject() : (char*)"(no subject)"); if (Template && strcmp(Template, Id) == 0) { Temp->Value(n); } } } n++; } } } return Temp; } class BrowseReply : public LDialog { List MsgIds; LCombo *Temp; public: - char *Arg; + LString Arg; BrowseReply(ScribeWnd *App, LView *Parent, const char *arg) { SetParent(Parent); LoadFromResource(IDD_FILTER_REPLY); MoveToCenter(); char *Template = LTokStr(arg); SkipSep(arg); char *All = LTokStr(arg); SkipSep(arg); char *MarkReplied = LTokStr(arg); if (All) { SetCtrlValue(IDC_ALL, atoi(All)); } if (MarkReplied) { SetCtrlValue(IDC_MARK_REPLIED, atoi(MarkReplied)); } Temp = LoadTemplates(App, this, MsgIds, IDC_TEMPLATE, Template); DeleteArray(Template); DeleteArray(MarkReplied); DeleteArray(All); } ~BrowseReply() { MsgIds.DeleteArrays(); } int OnNotify(LViewI *c, LNotification n) { switch (c->GetId()) { case IDOK: { if (Temp) { char *Template = MsgIds[(int)Temp->Value()]; int All = (int)GetCtrlValue(IDC_ALL); int MarkReplied = (int)GetCtrlValue(IDC_MARK_REPLIED); char s[256]; sprintf_s(s, sizeof(s), "\"%s\" %i %i", Template?Template:(char*)"", All, MarkReplied); - Arg = NewStr(s); + Arg = s; } // Fall thru } case IDCANCEL: { EndModal(c->GetId() == IDOK); break; } } return 0; } }; class BrowseForward : public LDialog { List MsgIds; ScribeWnd *App; LCombo *Temp; bool UseTemplate; public: - char *Arg; + LString Arg; BrowseForward(ScribeWnd *app, LView *parent, const char *arg, bool temp) { UseTemplate = temp; App = app; Arg = 0; Temp = 0; SetParent(parent); LoadFromResource(IDD_FILTER_FORWARD); MoveToCenter(); char *Template = 0; if (UseTemplate) { Template = LTokStr(arg); SkipSep(arg); } char *Email = LTokStr(arg); SkipSep(arg); char *Forward = LTokStr(arg); SkipSep(arg); char *MarkForwarded = LTokStr(arg); if (UseTemplate) { bool HasTemplate = ValidStr(Template) ? strlen(Template) > 1 : 0; SetCtrlValue(IDC_USE_TEMPLATE, HasTemplate); Temp = LoadTemplates(App, this, MsgIds, IDC_TEMPLATE, HasTemplate ? Template : 0); } else { LViewI *v = FindControl(IDC_USE_TEMPLATE); if (v) { int y1 = v->GetPos().y1; Children.Delete(v); DeleteObj(v); v = FindControl(IDC_TEMPLATE); if (v) { int y2 = v->GetPos().y2; Children.Delete(v); DeleteObj(v); int Sub = y1 - y2 - 10; for (auto v: Children) { LRect r = v->GetPos(); r.Offset(0, Sub); v->SetPos(r); } LRect r = GetPos(); r.y2 += Sub; SetPos(r); } } } SetCtrlName(IDC_EMAIL, Email); if (Forward) SetCtrlValue(IDC_ATTACHMENTS, atoi(Forward)); if (MarkForwarded) SetCtrlValue(IDC_MARK_FORWARDED, atoi(MarkForwarded)); DeleteArray(Template); DeleteArray(Email); DeleteArray(Forward); DeleteArray(MarkForwarded); } ~BrowseForward() { DeleteArray(Arg); } int OnNotify(LViewI *c, LNotification n) { switch (c->GetId()) { case IDOK: { char s[256]; bool HasTemplate = GetCtrlValue(IDC_USE_TEMPLATE) != 0; char *MsgId = HasTemplate ? MsgIds[(int)GetCtrlValue(IDC_TEMPLATE)] : 0; const char *Email = GetCtrlName(IDC_EMAIL); int Attachments = (int)GetCtrlValue(IDC_ATTACHMENTS); int MarkForwarded = (int)GetCtrlValue(IDC_MARK_FORWARDED); if (UseTemplate) { sprintf_s(s, sizeof(s), "\"%s\" \"%s\" %i %i", MsgId, Email?Email:(char*)"", Attachments, MarkForwarded); } else { sprintf_s(s, sizeof(s), "\"%s\" %i %i", Email?Email:(char*)"", Attachments, MarkForwarded); } - Arg = NewStr(s); + Arg = s; // Fall thru } case IDCANCEL: { EndModal(c->GetId() == IDOK); } } return 0; } }; class BrowseSaveAttach : public LDialog { ScribeWnd *App; public: char *Arg; BrowseSaveAttach(ScribeWnd *app, LView *parent, const char *arg) { Arg = 0; App = app; SetParent(parent); if (LoadFromResource(IDD_FILTER_SAVE_ATTACH)) { MoveToCenter(); char *Dir = LTokStr(arg); SkipSep(arg); char *Types = LTokStr(arg); SetCtrlName(IDC_DIR, Dir); SetCtrlName(IDC_TYPES, Types); DeleteArray(Dir); DeleteArray(Types); } } ~BrowseSaveAttach() { DeleteArray(Arg); } int OnNotify(LViewI *c, LNotification n) { switch (c->GetId()) { case IDC_BROWSE_DIR: { auto s = new LFileSelect(this); s->Name(GetCtrlName(IDC_DIR)); s->OpenFolder([&](auto dlg, auto status) { if (status) SetCtrlName(IDC_DIR, s->Name()); delete dlg; }); break; } case IDOK: { char s[512]; const char *Dir = GetCtrlName(IDC_DIR); const char *Types = GetCtrlName(IDC_TYPES); sprintf_s(s, sizeof(s), "\"%s\",\"%s\"", Dir?Dir:(char*)"", Types?Types:(char*)""); Arg = NewStr(s); // Fall thru } case IDCANCEL: { EndModal(c->GetId() == IDOK); } } return 0; } }; bool LgiCreateTempFileName(char *Path, int PathLen) { if (Path) { #if defined WIN32 int Len = GetTempPathA(PathLen, Path); #else strcpy(Path, "/tmp"); int Len = (int)strlen(Path); #endif if (Path[Len-1] != DIR_CHAR) strcat(Path, DIR_STR); int len = (int) strlen(Path); sprintf_s(Path+len, PathLen-len, "~%i.txt", LRand(10000)); return true; } return false; } ////////////////////////////////////////////////////////////// FilterCondition::FilterCondition() { Op = 0; Not = false; } FilterCondition &FilterCondition::operator=(FilterCondition &c) { Source.Reset(NewStr(c.Source)); Op = c.Op; Not = c.Not; Value.Reset(NewStr(c.Value)); return *this; } bool FilterCondition::Test(Filter *F, Mail *m, LStream *Log) { if (Log) Log->Print("\tCondition.Test Fld='%s'\n", (char*)Source); if (ValidStr(Source)) { int Flds = 0; for (; MailFieldDefs[Flds].FieldId; Flds++) { } LVariant v; // Get data if (_stricmp(Source, "mail.attachments") == 0) { // attachment(s) data List Attachments; if (m->GetAttachments(&Attachments)) { for (auto a: Attachments) { char *Data; ssize_t Length; LDateTime Temp; if (a->Get(&Data, &Length)) { // Zero terminate the string LVariant v; v.SetBinary(Length, Data); // Test the file if (TestData(F, v, Log)) { return true; } } } } return false; } else if (_stricmp(Source, "mail.attachmentnames") == 0) { // attachment(s) name List Attachments; // ItemFieldDef AttachType = {"Attachment(s) Name", SdAttachmentNames, GV_STRING}; LDateTime Temp; if (m->GetAttachments(&Attachments)) { for (auto a: Attachments) { LVariant v = a->GetName(); if (TestData(F, v, Log)) { return true; } } } return false; } else { bool Status = false; ItemFieldDef *f = 0; if (_stricmp(Source, "mail.*") == 0) { ItemFieldDef *Start = MailFieldDefs; ItemFieldDef *End = MailFieldDefs + Flds - 1; for (f = Start; f <= End && !Status; f++) { switch (f->FieldId) { case FIELD_TO: { for (LDataPropI *a = m->GetTo()->First(); a; a = m->GetTo()->Next()) { char Data[256]; sprintf_s(Data, sizeof(Data), "%s <%s>", a->GetStr(FIELD_NAME), a->GetStr(FIELD_EMAIL)); LVariant v(Data); if (TestData(F, v, Log)) { Status |= true; } } continue; break; } case FIELD_FROM: { char Data[256]; sprintf_s(Data, sizeof(Data), "%s <%s>", m->GetFrom()->GetStr(FIELD_NAME), m->GetFrom()->GetStr(FIELD_EMAIL)); v = Data; break; } case FIELD_REPLY: { char Data[256]; sprintf_s(Data, sizeof(Data), "%s <%s>", m->GetReply()->GetStr(FIELD_NAME), m->GetReply()->GetStr(FIELD_EMAIL)); v = Data; break; } case FIELD_SUBJECT: v = m->GetSubject(); break; case FIELD_SIZE: v = m->TotalSizeof(); break; case FIELD_DATE_RECEIVED: v = m->GetDateReceived(); break; case FIELD_DATE_SENT: v = m->GetDateSent(); break; case FIELD_TEXT: v = m->GetBody(); break; case FIELD_INTERNET_HEADER: v = m->GetInternetHeader(); break; case FIELD_MESSAGE_ID: v = m->GetMessageId(); break; case FIELD_PRIORITY: v = m->GetPriority(); break; case FIELD_ALTERNATE_HTML: v = m->GetHtml(); break; case FIELD_LABEL: v = m->GetLabel(); break; } } } else { if (F) { F->GetValue(Source, v); } else if (Log) { Log->Print("%s:%i - Error: No filter to query value.\n", __FILE__, __LINE__); } } if (v.Type) { // Test data Status |= TestData(F, v, Log); } else if (Log) { Log->Print("%s:%i - Error: Variant doesn't have type!\n", __FILE__, __LINE__); } return Status; } } return false; } char *LogPreview(char *s) { LStringPipe p(1 << 10); if (s) { char *c; for (c = s; *c && c - s < 200; c++) { switch (*c) { case '\n': p.Push("\\n"); break; case '\r': p.Push("\\r"); break; case '\t': p.Push("\\t"); break; default: { p.Push(c, 1); } } } if (*c) { p.Push("..."); } } return p.NewStr(); } bool FilterCondition::TestData(Filter *F, LVariant &Var, LStream *Log) { // Do DOM lookup on the Value LVariant Val; if (F && F->Evaluate(Value, Val)) { // Compare using type switch (Var.Type) { case GV_LIST: { for (auto v: *Var.Value.Lst) { if (TestData(F, *v, Log)) { return true; } } break; } case GV_DOM: { // Probably an address field LVariant n; if (Var.Value.Dom->GetValue("Name", n)) { if (TestData(F, n, Log)) { return true; } } if (Var.Value.Dom->GetValue("Email", n)) { if (TestData(F, n, Log)) { return true; } } break; } case GV_STRING: { char *sVar = Var.Str(); char *sVal = Val.Str(); bool IsStr = ValidStr(sVar); bool IsVal = ValidStr(sVal); char *VarLog = Log ? LogPreview(sVar) : 0; bool m = false; switch (Op) { case OP_EQUAL: { if (!IsStr && !IsVal) { m = true; } else if (IsStr && IsVal) { m = _stricmp(sVal, sVar) == 0; } if (Log) Log->Print("\t\t\t'%s' == '%s' = %i\n", VarLog, sVal, m); break; } case OP_LIKE: { m = MatchStr(sVal, sVar); if (Log) Log->Print("\t\t\t'%s' like '%s' = %i\n", VarLog, sVal, m); break; } case OP_CONTAINS: { if (IsVal && IsStr) { m = stristr(sVar, sVal) != 0; if (Log) Log->Print("\t\t\t'%s' contains '%s' = %i\n", VarLog, sVal, m); } break; } case OP_STARTS_WITH: { if (IsVal && IsStr) { size_t Len = strlen(sVal); m = _strnicmp(sVar, sVal, Len) == 0; if (Log) Log->Print("\t\t\t'%s' starts with '%s' = %i\n", VarLog, sVal, m); } break; } case OP_ENDS_WITH: { if (IsVal && IsStr) { size_t SLen = strlen(sVar); size_t VLen = strlen(sVal); if (SLen >= VLen) { m = _strnicmp(sVar + SLen - VLen, sVal, VLen) == 0; if (Log) Log->Print("\t\t\t'%s' ends with '%s' = %i\n", VarLog, sVal, m); } else { if (Log) Log->Print("\t\t\tEnds With Error: '%s' is shorter than '%s'\n", sVar, sVal); } } else { if (Log) Log->Print("\t\t\tEnds With Error: invalid arguments\n"); } break; } } DeleteArray(VarLog); return m; break; } case GV_INT32: { // Convert Val to int int Int = 0; if (Val.Str()) { Int = atoi(Val.Str()); } else if (Val.Type == GV_INT32) { Int = Val.Value.Int; } int IntVal = Var.Value.Int; switch (Op) { case OP_LIKE: // for lack anything better case OP_EQUAL: { bool m = Int == IntVal; if (Log) Log->Print("\t\t\t%i == %i = %i\n", Int, IntVal, m); return m; } case OP_NOT_EQUAL: { bool m = Int != IntVal; if (Log) Log->Print("\t\t\t%i != %i = %i\n", Int, IntVal, m); return m; } case OP_LESS_THAN: { bool m = Int < IntVal; if (Log) Log->Print("\t\t\t%i < %i = %i\n", Int, IntVal, m); return m; } case OP_LESS_THAN_OR_EQUAL: { bool m = Int <= IntVal; if (Log) Log->Print("\t\t\t%i <= %i = %i\n", Int, IntVal, m); return m; } case OP_GREATER_THAN: { bool m = Int > IntVal; if (Log) Log->Print("\t\t\t%i > %i = %i\n", Int, IntVal, m); return m; } case OP_GREATER_THAN_OR_EQUAL: { bool m = Int >= IntVal; if (Log) Log->Print("\t\t\t%i >= %i = %i\n", Int, IntVal, m); return m; } } break; } case GV_DATETIME: { LDateTime Temp; LDateTime *DVal; if (Val.Type == GV_DATETIME) { DVal = Val.Value.Date; } else if (Val.Type == GV_STRING) { Temp.Set(Val.Str()); DVal = &Temp; } else break; LDateTime *DVar = Var.Value.Date; if (DVal && DVar) { bool Less = *DVar < *DVal; bool Greater = *DVar > *DVal; bool Equal = !Less && !Greater; char LogVal[64]; char LogVar[64]; if (Log) { DVal->Get(LogVal, sizeof(LogVal)); DVar->Get(LogVar, sizeof(LogVar)); } switch (Op) { case OP_LIKE: case OP_EQUAL: { if (Log) Log->Print("\t\t\t%s = %s == %i\n", LogVar, LogVal, Equal); return Equal; } case OP_NOT_EQUAL: { if (Log) Log->Print("\t\t\t%s != %s == %i\n", LogVar, LogVal, !Equal); return !Equal; } case OP_LESS_THAN: { if (Log) Log->Print("\t\t\t%s <= %s == %i\n", LogVar, LogVal, Less); return Less; } case OP_LESS_THAN_OR_EQUAL: { if (Log) Log->Print("\t\t\t%s < %s == %i\n", LogVar, LogVal, Less || Equal); return Less || Equal; } case OP_GREATER_THAN: { if (Log) Log->Print("\t\t\t%s > %s == %i\n", LogVar, LogVal, Greater); return Greater; } case OP_GREATER_THAN_OR_EQUAL: { if (Log) Log->Print("\t\t\t%s >= %s == %i\n", LogVar, LogVal, Greater || Equal); return Greater || Equal; } } } break; } default: { if (Log) Log->Print("\t\t\tUnknown data type %i.\n", Var.Type); break; } } } return false; } ThingUi *FilterCondition::DoUI(MailContainer *c) { return NULL; } ////////////////////////////////////////////////////////////// // #define OPT_Action "Action" #define OPT_Type "Type" #define OPT_Arg1 "Arg1" #define IDC_TYPE_CBO 2000 #define IDC_ARG_EDIT 2001 #define IDC_BROWSE_ARG 2002 FilterAction::FilterAction(LDataStoreI *Store) { Type = ACTION_MOVE_TO_FOLDER; TypeCbo = 0; ArgEdit = 0; Btn = 0; } FilterAction::~FilterAction() { DeleteObj(TypeCbo); DeleteObj(ArgEdit); DeleteObj(Btn); } int FilterAction::OnNotify(LViewI *c, LNotification n) { switch (c->GetId()) { case IDC_TYPE_CBO: { Type = (FilterActionTypes) c->Value(); break; } case IDC_ARG_EDIT: { - Arg1.Reset(NewStr(c->Name())); + Arg1 = c->Name(); break; } case IDC_BROWSE_ARG: { if (ArgEdit) ArgEdit->Name(Arg1); break; } } return 0; } void FilterAction::OnMeasure(LPoint *Info) { LListItem::OnMeasure(Info); if (Select()) Info->y += 2; } bool FilterAction::Select() { return LListItem::Select(); } void FilterAction::OnPaintColumn(LItem::ItemPaintCtx &Ctx, int i, LItemColumn *c) { LListItem::OnPaintColumn(Ctx, i, c); if (LListItem::Select() && !TypeCbo && !ArgEdit) Select(true); else if (i == 0 && TypeCbo) TypeCbo->SetPos(*GetPos(i)); else if (i == 1 && ArgEdit) ArgEdit->SetPos(*GetPos(i)); else if (i == 2 && Btn) Btn->SetPos(*GetPos(i)); } void FilterAction::Select(bool b) { LListItem::Select(b); if (b) { LList *Lst = LListItem::GetList(); if (Lst && Lst->IsAttached()) { LRect *r = GetPos(0); if (!TypeCbo) { TypeCbo = new LCombo(IDC_TYPE_CBO, r->x1, r->y1, r->X(), r->Y(), 0); for (int i=0; ActionNames[i].Id; i++) TypeCbo->Insert(LLoadString(ActionNames[i].Id)); TypeCbo->Attach(Lst); } TypeCbo->Value(Type); TypeCbo->SetPos(*r); r = GetPos(1); if (!ArgEdit) { ArgEdit = new LEdit(IDC_ARG_EDIT, r->x1, r->y1, r->X(), r->Y(), 0); ArgEdit->Attach(Lst); } ArgEdit->Name(Arg1); ArgEdit->SetPos(*r); r = GetPos(2); if (!Btn) { Btn = new LButton(IDC_BROWSE_ARG, r->x1, r->y1, r->X(), r->Y(), "..."); Btn->Attach(Lst); } Btn->SetPos(*r); } } else { DeleteObj(TypeCbo); DeleteObj(ArgEdit); DeleteObj(Btn); } } const char *FilterAction::GetText(int Col) { switch (Col) { case 0: return (char*)LLoadString(ActionNames[Type].Id); break; case 1: return Arg1; break; case 2: break; } return 0; } bool FilterAction::Get(LXmlTag *t) { if (!t) return false; // Obj -> XML t->SetAttr(OPT_Type, Type); t->SetAttr(OPT_Arg1, Arg1); return true; } bool FilterAction::Set(LXmlTag *t) { if (!t) return false; // XML -> Obj Type = (FilterActionTypes) t->GetAsInt(OPT_Type); - Arg1.Reset(NewStr(t->GetAttr(OPT_Arg1))); + Arg1 = t->GetAttr(OPT_Arg1); return true; } LDataPropI &FilterAction::operator =(LDataPropI &p) { FilterAction *c = dynamic_cast(&p); if (c) { Type = c->Type; - Arg1.Reset(NewStr(c->Arg1)); + Arg1 = c->Arg1; } return *this; } ThingUi *FilterAction::DoUI(MailContainer *c) { return 0; } const char *GetFileName(const char *Path) { if (Path) { auto d = strrchr(Path, DIR_CHAR); if (d) return d + 1; else return Path; } return 0; } -bool CollectAttachmentsByPattern(Mail *m, char *Pattern, List &Files) +bool CollectAttachmentsByPattern(Mail *m, LString Pattern, List &Files) { Files.Empty(); if (m) { List Attachments; if (m->GetAttachments(&Attachments)) { - LToken p(Pattern, " ,;"); + auto p = Pattern.SplitDelimit(" ,;"); for (auto a: Attachments) { bool Match = true; for (unsigned i=0; Match && iGetName()); if (d) Match = MatchStr(p[i], d); } if (Match) Files.Insert(a); } } } return Files[0] != 0; } Mail *GetTemplateMail(ScribeWnd *App, char *TemplateMsgId) { ScribeFolder *Templates = App->GetFolder(FOLDER_TEMPLATES); if (Templates) { // Mail *Template = 0; for (auto t: Templates->Items) { Mail *m = t->IsMail(); if (m) { auto MsgId = m->GetMessageId(); if (MsgId && strcmp(MsgId, TemplateMsgId) == 0) { return m; break; } } } } return 0; } class FilterScribeDom : public ScribeDom { public: FilterScribeDom(ScribeWnd *a) : ScribeDom(a) { } bool GetVariant(const char *Name, LVariant &Value, const char *Array = 0) { if (!Name) return false; if (!_stricmp(Name, "file")) { LVariant v; if (!GetValue(Array, v)) return false; char p[MAX_PATH_LEN], fn[32]; do { sprintf_s(fn, sizeof(fn), "file_%d.tmp", LRand()); LMakePath(p, sizeof(p), ScribeTempPath(), fn); } while (LFileExists(p)); LFile f; if (f.Open(p, O_WRITE)) { switch (v.Type) { case GV_INT32: f.Print("%i", v.Value.Int); break; case GV_INT64: f.Print(LPrintfInt64, v.Value.Int64); break; case GV_BOOL: f.Print("%s", v.Value.Bool ? "true" : "false"); break; case GV_DOUBLE: f.Print("%f", v.Value.Dbl); break; case GV_STRING: case GV_WSTRING: f.Print("%s", v.Str()); break; case GV_BINARY: f.Write(v.Value.Binary.Data, v.Value.Binary.Length); break; case GV_DATETIME: v.Value.Date->Get(fn, sizeof(fn)); f.Write(fn, strlen(fn)); break; default: f.Print("Unsupported type."); break; } f.Close(); Value = p; return true; } } return ScribeDom::GetVariant(Name, Value, Array); } }; bool FilterAction::Do(Filter *F, ScribeWnd *App, Mail *&m, LStream *Log) { bool Status = false; if (!F || !App || !m) { LAssert(!"Param error."); return false; } switch (Type) { case ACTION_MOVE_TO_FOLDER: { ScribeFolder *Folder = App->GetFolder(Arg1); if (Folder) { LArray Items; Items.Add(m); Status = Folder->MoveTo(Items); m = Items[0]->IsMail(); if (Log) Log->Print("\tACTION_MOVE_TO_FOLDER(%s) = %i.\n", Arg1.Get(), Status); } else if (Log) { Log->Print("\tACTION_MOVE_TO_FOLDER(%s) failed, folder missing.\n", Arg1.Get()); } break; } case ACTION_COPY: { ScribeFolder *Folder = App->GetFolder(Arg1); if (Folder) { LArray Items; Items.Add(m); Status = Folder->MoveTo(Items, true); m = Items[0]->IsMail(); if (Log) Log->Print("\tACTION_COPY(%s) = %i.\n", Arg1.Get(), Status); } else if (Log) { Log->Print("\tACTION_COPY(%s) failed, folder missing.\n", Arg1.Get()); } break; } case ACTION_EXPORT: { LFile::Path p(Arg1); if (!p.IsFolder()) { if (Log) Log->Print("\tACTION_EXPORT(%s) failed, folder missing.\n", Arg1.Get()); break; } auto Fn = m->GetDropFileName(); p += LGetLeaf(Fn); LAutoPtr out(new LFile); if (!out->Open(p, O_WRITE)) { if (Log) Log->Print("\tACTION_EXPORT(%s) failed: couldn't open file: %s.\n", Arg1.Get(), p.GetFull().Get()); break; } if (!m->Export(m->AutoCast(out), sMimeMessage)) { if (Log) Log->Print("\tACTION_EXPORT(%s) failed: couldn't export file.\n", Arg1.Get()); } break; } case ACTION_DELETE: { bool Local = ValidStr(Arg1) ? stristr(Arg1, "local") != 0 : true; bool Server = stristr(Arg1, "server") != 0; if (Server) { ScribeAccount *a = m->GetAccountSentTo(); if (!a) break; auto Uid = m->GetServerUid(); if (Uid.Str()) { if (Log) Log->Print("\tACTION_DELETE - Setting '%s' to be deleted on the server (Uid=%s)\n", m->GetSubject(), Uid.Str()); a->Receive.DeleteAsSpam(Uid.Str()); m->SetServerUid(Uid = NULL); } else { LVariant Uid; if (m->GetValue("InternetHeader[X-UIDL]", Uid)) { if (Log) Log->Print("\tACTION_DELETE - Setting '%s' to be deleted on the server (Uid=%s)\n", m->GetSubject(), Uid.Str()); a->Receive.DeleteAsSpam(Uid.Str()); } } } if (Local) { bool DeleteStatus = m->OnDelete(); if (Log) Log->Print("\tACTION_DELETE(%s) status %i.\n", Arg1.Get(), DeleteStatus); /* ScribeFolder *Folder = App->GetFolder(FOLDER_TRASH); if (Folder) { Thing *t = m; Status = Folder->MoveTo(t); m = t->IsMail(); if (Log) Log->Print("\tACTION_DELETE(%s) = %i.\n", Arg1.Get(), Status); } else if (Log) { Log->Print("\tACTION_DELETE(%s) failed, trash missing.\n", Arg1.Get()); } */ } break; } case ACTION_SET_READ: { bool Read = true; bool NotNew = false; if (ValidStr(Arg1)) { if (IsDigit(*Arg1)) { // boolean number - Read = atoi(Arg1) != 0; + Read = Arg1.Int() != 0; } else if (stristr(Arg1, "true")) { Read = true; } else if (stristr(Arg1, "false")) { Read = false; } if (stristr(Arg1, "notnew")) { NotNew = true; } } int f = m->GetFlags(); if (Read) SetFlag(f, MAIL_READ); else ClearFlag(f, MAIL_READ); m->SetFlags(f); if (Log) Log->Print("\tACTION_SET_READ(%s)\n", Arg1.Get()); if (NotNew) { List Objs; Objs.Insert(m); App->OnNewMail(&Objs, false); } break; } case ACTION_LABEL: { LVariant v; if (!F || !F->GetValue(Arg1, v)) v = Arg1; m->SetVariant("Label", v); break; } case ACTION_EMPTY_FOLDER: { if (F->App && Arg1) { ScribeFolder *Folder = F->App->GetFolder(Arg1); if (Folder) { Folder->LoadThings(); List m; Thing *t; while ((t = Folder->Items[0])) { if (t->IsMail()) { m.Insert(t->IsMail()); } if (Folder->DeleteThing(t)) { DeleteObj(t); } } F->App->OnNewMail(&m, false); Folder->OnUpdateUnRead(0, true); if (Log) Log->Print("\tACTION_EMPTY_FOLDER(%s)\n", Arg1.Get()); } } break; } case ACTION_MARK_AS_SPAM: { m->DeleteAsSpam(App); break; } case ACTION_PRINT: { LPrinter Info; #ifdef _MSC_VER #pragma message ("Warning: ACTION_PRINT not implemented.") #endif /* FIXME if (Info.Serialize(Arg1, false)) { App->ThingPrint(m, &Info); } */ break; } case ACTION_PLAY_SOUND: { LPlaySound(Arg1, true); break; } case ACTION_EXECUTE: { FilterScribeDom dom(App); dom.Email = m; dom.Fil = F; LAutoString cmd(ScribeInsertFields(Arg1, &dom)); if (cmd) { const char *s = cmd; LAutoString exe(LTokStr(s)); LExecute(exe, s); } break; } case ACTION_OPEN: { m->DoUI(); break; } case ACTION_MARK: { - if (_stricmp(Arg1, "false") == 0) + if (Stricmp(Arg1.Get(), "false") == 0) { // unmark the item... m->SetMarkColour(0); } else { // parse out RGB - LToken T(Arg1, ","); + auto T = Arg1.SplitDelimit(","); if (T.Length() == 3) { // we have an RGB, so set it baby uint32_t c = Rgb32(atoi(T[0]), atoi(T[1]), atoi(T[2])); m->SetMarkColour(c); } else { uint32_t c = Rgb32(0, 0, 255); m->SetMarkColour(c); } } break; } case ACTION_REPLY: { if (Arg1) { const char *s = Arg1; char *TemplateMsgId = LTokStr(s); SkipSep(s); char *ReplyAll = LTokStr(s); SkipSep(s); char *MarkReplied = LTokStr(s); Mail *Template = GetTemplateMail(App, TemplateMsgId); if (Template) { Thing *t = App->CreateThingOfType(MAGIC_MAIL); if (t) { Mail *n = t->IsMail(); if (n) { bool MarkOriginal = MarkReplied && atoi(MarkReplied); // Prepare mail... n->OnReply(m, ValidStr(ReplyAll)?atoi(ReplyAll)!=0:false, MarkOriginal); n->SetFlags(m->GetFlags() | MAIL_READY_TO_SEND); if (ValidStr(Template->GetSubject())) { n->SetSubject(Template->GetSubject()); } n->SetBody(ScribeInsertFields(Template->GetBody(), F)); // Save it... n->Save(0); // Send it... if not offline. LVariant Offline; App->GetOptions()->GetValue(OPT_WorkOffline, Offline); if (!Offline.CastInt32()) { App->PostEvent(M_COMMAND, IDM_SEND_MAIL, 0); } } else DeleteObj(t); } } DeleteArray(TemplateMsgId); DeleteArray(ReplyAll); DeleteArray(MarkReplied); } break; } case ACTION_FORWARD: { const char *s = Arg1; char *TemplateMsgId = LTokStr(s); SkipSep(s); char *Email = LTokStr(s); SkipSep(s); char *Attach = LTokStr(s); SkipSep(s); char *MarkForwarded = LTokStr(s); bool Attachments = Attach ? atoi(Attach)!=0 : true; if (ValidStr(Email)) { Thing *t = App->CreateThingOfType(MAGIC_MAIL); if (t) { Mail *n = t->IsMail(); if (n) { bool MarkOriginal = MarkForwarded && atoi(MarkForwarded); // Setup email... Mail *Template = TemplateMsgId ? GetTemplateMail(App, TemplateMsgId) : 0; if (Template) { n->SetSubject(Template->GetSubject()); if (ValidStr(Template->GetBody())) { n->SetBody(ScribeInsertFields(Template->GetBody(), F)); } if (Attachments) { List Att; m->GetAttachments(&Att); for (auto a: Att) { n->AttachFile(new Attachment(App, a)); } } } else { n->OnForward(m, MarkOriginal, Attachments); } n->SetFlags(m->GetFlags() | MAIL_READY_TO_SEND); LDataPropI *Addr = n->GetTo()->Create(n->GetObject()->GetStore()); if (Addr) { LVariant v; if (F->GetValue(Email, v)) { Addr->SetStr(FIELD_EMAIL, v.Str()); } else { Addr->SetStr(FIELD_EMAIL, Email); } n->GetTo()->Insert(Addr); } // Save it... n->Save(0); // Send it... if not offline. LVariant Offline; App->GetOptions()->GetValue(OPT_WorkOffline, Offline); if (!Offline.CastInt32()) { App->PostEvent(M_COMMAND, IDM_SEND_MAIL, 0); } } else DeleteObj(t); } } DeleteArray(Email); DeleteArray(Attach); DeleteArray(MarkForwarded); break; } case ACTION_BOUNCE: { const char *s = Arg1; char *Email = LTokStr(s); SkipSep(s); char *Attach = LTokStr(s); SkipSep(s); char *Mark = LTokStr(s); if (ValidStr(Email)) { Thing *t = App->CreateThingOfType(MAGIC_MAIL); if (t) { Mail *n = t->IsMail(); if (n) { bool Attachments = Attach && atoi(Attach); bool MarkOriginal = Mark && atoi(Mark); // Setup email... n->OnBounce(m, MarkOriginal, Attachments); n->SetFlags(m->GetFlags() | MAIL_READY_TO_SEND); LDataPropI *Addr = n->GetTo()->Create(n->GetObject()->GetStore()); if (Addr) { LVariant v; if (F->GetValue(Email, v)) { Addr->SetStr(FIELD_EMAIL, v.Str()); } else { Addr->SetStr(FIELD_EMAIL, Email); } n->GetTo()->Insert(Addr); } // Save it... n->Save(0); // Send it... if not offline. LVariant Offline; App->GetOptions()->GetValue(OPT_WorkOffline, Offline); if (!Offline.CastInt32()) { App->PostEvent(M_COMMAND, IDM_SEND_MAIL, 0); } } else DeleteObj(t); } } DeleteArray(Email); DeleteArray(Attach); DeleteArray(Mark); break; } case ACTION_SAVE_ATTACHMENTS: { const char *arg = Arg1; char *Dir = LTokStr(arg); SkipSep(arg); char *Types = LTokStr(arg); List Files; if (CollectAttachmentsByPattern(m, Types, Files)) { for (auto a: Files) { auto d = GetFileName(a->GetName()); if (d) { char Path[256]; LMakePath(Path, sizeof(Path), Dir, d); a->SaveTo(Path); } } } DeleteArray(Dir); DeleteArray(Types); break; } case ACTION_DELETE_ATTACHMENTS: { List Files; if (CollectAttachmentsByPattern(m, Arg1, Files)) { for (auto a: Files) { m->DeleteAttachment(a); } } break; } case ACTION_CHANGE_CHARSET: { m->SetBodyCharset(Arg1); break; } } return Status; } int CsCmp(LCharset **a, LCharset **b) { return _stricmp((*a)->Charset, (*b)->Charset); } void FilterAction::DescribeHtml(Filter *Flt, LStream &s) { s.Print("%s ", GetText(0)); switch (Type) { case ACTION_MOVE_TO_FOLDER: { ScribeFolder *Folder = Flt->App->GetFolder(Arg1); if (Folder) s.Print("\"%s\"\n", Arg1.Get()); else s.Print("\"%s\"\n", Arg1.Get()); break; } case ACTION_DELETE: break; case ACTION_PRINT: break; case ACTION_PLAY_SOUND: break; case ACTION_OPEN: break; case ACTION_EXECUTE: break; case ACTION_MARK: break; case ACTION_SET_READ: break; case ACTION_LABEL: break; case ACTION_EMPTY_FOLDER: break; case ACTION_MARK_AS_SPAM: break; case ACTION_REPLY: break; case ACTION_FORWARD: break; case ACTION_BOUNCE: break; case ACTION_SAVE_ATTACHMENTS: break; case ACTION_DELETE_ATTACHMENTS: break; case ACTION_CHANGE_CHARSET: break; case ACTION_COPY: break; case ACTION_EXPORT: break; } } void FilterAction::Browse(ScribeWnd *App, LView *Parent) { if (!Parent) return; switch (Type) { default: LAssert(0); break; case ACTION_MOVE_TO_FOLDER: case ACTION_COPY: case ACTION_EMPTY_FOLDER: { auto Dlg = new FolderDlg(Parent, App, MAGIC_MAIL); Dlg->DoModal([this, Dlg](auto dlg, auto id) { if (id) - Arg1.Reset(NewStr(Dlg->Get())); + Arg1 = Dlg->Get(); delete dlg; }); break; } case ACTION_EXPORT: { auto s = new LFileSelect(Parent); - s->OpenFolder([&](auto dlg, auto status) + s->OpenFolder([this](auto s, auto status) { if (status) - Arg1.Reset(NewStr(s->Name())); - delete dlg; + Arg1 = s->Name(); + delete s; }); break; } case ACTION_DELETE: { auto RClick = new LSubMenu; if (RClick) { RClick->AppendItem("Local (default)", IDM_LOCAL, true); RClick->AppendItem("From Server", IDM_SERVER, true); RClick->AppendItem("Local and from Server", IDM_LOCAL_AND_SERVER, true); LMouse m; if (Parent->GetMouse(m, true)) { switch (RClick->Float(Parent, m.x, m.y)) { case IDM_LOCAL: { - Arg1.Reset(NewStr("local")); + Arg1 = "local"; break; } case IDM_SERVER: { - Arg1.Reset(NewStr("server")); + Arg1 = "server"; break; } case IDM_LOCAL_AND_SERVER: { - Arg1.Reset(NewStr("local,server")); + Arg1 = "local,server"; break; } } } DeleteObj(RClick); } break; } case ACTION_OPEN: { // no configuration break; } case ACTION_SET_READ: { auto RClick = new LSubMenu; if (RClick) { RClick->AppendItem("Read", IDM_TRUE, true); RClick->AppendItem("Unread", IDM_FALSE, true); RClick->AppendItem("Unread But Not New", IDM_NOTNEW, true); LMouse m; if (Parent->GetMouse(m, true)) { switch (RClick->Float(Parent, m.x, m.y)) { case IDM_TRUE: { - Arg1.Reset(NewStr("true")); + Arg1 = "true"; break; } case IDM_FALSE: { - Arg1.Reset(NewStr("false")); + Arg1 = "false"; break; } case IDM_NOTNEW: { - Arg1.Reset(NewStr("false,notnew")); + Arg1 = "false,notnew"; break; } } } DeleteObj(RClick); } break; } case ACTION_MARK: { auto RClick = new LSubMenu; if (RClick) { BuildMarkMenu(RClick, MS_One, 0); LMouse m; if (Parent->GetMouse(m, true)) { int Result = RClick->Float(Parent, m.x, m.y); if (Result == IDM_UNMARK) { - Arg1.Reset(NewStr("False")); + Arg1 = "False"; } else if (Result >= IDM_MARK_BASE) { char s[32]; sprintf_s(s, sizeof(s), "%i,%i,%i", R32(MarkColours32[Result-IDM_MARK_BASE]), G32(MarkColours32[Result-IDM_MARK_BASE]), B32(MarkColours32[Result-IDM_MARK_BASE])); - Arg1.Reset(NewStr(s)); + Arg1 = s; } } } break; } case ACTION_PRINT: { LPrinter Info; #ifdef _MSC_VER #pragma message ("Warning: ACTION_PRINT not implemented.") #endif /* Info.Serialize(Arg1, false); if (Info.Browse(Parent)) { Info.Serialize(Arg1, true); } */ break; } case ACTION_PLAY_SOUND: case ACTION_EXECUTE: { auto Select = new LFileSelect(Parent); Select->Parent(Parent); if (Type == ACTION_PLAY_SOUND) { Select->Type("Wave files", "*.wav"); } else { Select->Type("Executables", "*.exe"); Select->Type("All Files", LGI_ALL_FILES); } Select->Name(Arg1); - Select->Open([this](auto dlg, auto id) + Select->Open([this](auto s, auto ok) { - if (id) - Arg1.Reset(NewStr(dlg->Name())); - delete dlg; + if (ok) + Arg1 = s->Name(); + delete s; }); break; } case ACTION_REPLY: { auto Dlg = new BrowseReply(App, Parent, Arg1); Dlg->DoModal([this, Dlg](auto dlg, auto id) { if (id) - Arg1.Reset(NewStr(Dlg->Arg)); + Arg1 = Dlg->Arg; delete dlg; }); break; } case ACTION_FORWARD: { auto Dlg = new BrowseForward(App, Parent, Arg1, true); Dlg->DoModal([this, Dlg](auto dlg, auto id) { if (id) - Arg1.Reset(NewStr(Dlg->Arg)); + Arg1 = Dlg->Arg; delete dlg; }); break; } case ACTION_BOUNCE: { auto Dlg = new BrowseForward(App, Parent, Arg1, false); Dlg->DoModal([this, Dlg](auto dlg, auto id) { if (id) - Arg1.Reset(NewStr(Dlg->Arg)); + Arg1 = Dlg->Arg; delete dlg; }); break; } case ACTION_SAVE_ATTACHMENTS: { auto Dlg = new BrowseSaveAttach(App, Parent, Arg1); Dlg->DoModal([this, Dlg](auto dlg, auto id) { if (id) - Arg1.Reset(NewStr(Dlg->Arg)); + Arg1 = Dlg->Arg; delete dlg; }); break; } case ACTION_CHANGE_CHARSET: { auto s = new LSubMenu; if (s) { LArray Cs; for (LCharset *c = LGetCsList(); c->Charset; c++) { Cs.Add(c); } Cs.Sort(CsCmp); for (unsigned i=0; iCharset[0] != Cs[i]->Charset[0]) break; n++; } if (n > 1) { char a[64]; char *One = LSeekUtf8(Cs[i]->Charset, 1); ssize_t Len = One - Cs[i]->Charset; memcpy(a, Cs[i]->Charset, Len); strcpy_s(a + Len, sizeof(a) - Len, "..."); auto Sub = s->AppendSub(a); if (Sub) { for (unsigned k=0; kAppendItem(Cs[i+k]->Charset, i+k+1, Cs[i+k]->IsAvailable()); } i += n - 1; } } else { s->AppendItem(Cs[i]->Charset, i+1, Cs[i]->IsAvailable()); } } LMouse m; Parent->GetMouse(m, true); int Result = s->Float(Parent, m.x, m.y, true); if (Result) { Result--; if (Result >= 0 && Result < (int)Cs.Length()) { - Arg1.Reset(NewStr(Cs[Result]->Charset)); + Arg1 = Cs[Result]->Charset; } } DeleteObj(s); } break; } } } /////////////////////////////////////////////////////////////// int Filter::MaxIndex = -1; Filter::Filter(ScribeWnd *window, LDataI *object) : Thing(window, object) { DefaultObject(object); d = new FilterPrivate; Ui = 0; Current = 0; IgnoreCheckEvents = true; ChkIncoming = new LListItemCheckBox(this, 2, GetIncoming()!=0); ChkOutgoing = new LListItemCheckBox(this, 3, GetOutgoing()!=0); ChkInternal = new LListItemCheckBox(this, 4, GetInternal()!=0); IgnoreCheckEvents = false; } Filter::~Filter() { Empty(); DeleteObj(d); } bool Filter::GetFormats(bool Export, LString::Array &MimeTypes) { MimeTypes.Add(sTextXml); return MimeTypes.Length() > 0; } char *Filter::GetDropFileName() { if (!DropFileName) { auto Nm = GetName(); char f[256]; if (Nm) { LUtf8Ptr o(f); for (LUtf8Ptr i(Nm); (uint32_t)i; i++) { if (!strchr(LGI_IllegalFileNameChars, (uint32_t)i)) o.Add(i); } o.Add(0); } else strcpy_s(f, sizeof(f), "Filter"); strcat_s(f, sizeof(f), ".xml"); DropFileName.Reset(NewStr(f)); } return DropFileName; } bool Filter::GetDropFiles(LString::Array &Files) { char Tmp[MAX_PATH_LEN]; LMakePath(Tmp, sizeof(Tmp), ScribeTempPath(), GetDropFileName()); LAutoPtr Out(new LFile); if (!Out->Open(Tmp, O_WRITE)) return false; if (!Export(AutoCast(Out), sTextXml)) return false; Files.Add(Tmp); return true; } Thing::IoProgress Filter::Import(IoProgressImplArgs) { if (Stricmp(mimeType, sTextXml) && Stricmp(mimeType, sMimeXml)) IoProgressNotImpl(); LXmlTree Tree; LXmlTag r; if (!Tree.Read(&r, stream)) IoProgressError("Xml parse error."); if (!r.IsTag("Filter")) IoProgressError("No filter tag."); Empty(); LXmlTag *t = r.GetChildTag("Name"); if (t && t->GetContent()) SetName(t->GetContent()); SetIndex(r.GetAsInt("index")); if ((t = r.GetChildTag(ELEMENT_CONDITIONS))) { LStringPipe p; if (Tree.Write(t, &p)) { LAutoString s(p.NewStr()); ConditionsCache.Reset(); SetConditionsXml(s); } if ((t = r.GetChildTag("Actions"))) { LStringPipe p; if (Tree.Write(t, &p)) { LAutoString s(p.NewStr()); SetActionsXml(s); } else IoProgressError("Xml write failed."); } } IoProgressSuccess(); } Thing::IoProgress Filter::Export(IoProgressImplArgs) { if (Stricmp(mimeType, sMimeXml)) IoProgressNotImpl(); LXmlTag r("Filter"); LXmlTag *t; if ((t = r.CreateTag("Name"))) t->SetContent(GetName()); r.SetAttr("index", GetIndex()); LAutoPtr Cond = Parse(false); r.InsertTag(Cond.Release()); LAutoPtr Act = Parse(true); r.InsertTag(Act.Release()); LXmlTree tree; if (!tree.Write(&r, stream)) IoProgressError("Failed to write xml."); IoProgressSuccess(); } /// This filters a list of email. The email will have it's NewEmail state set to /// one of three things: /// If the email is not filtered then: /// Mail::NewEmailBayes /// If the email is filtered but not set to !NEW then: /// Mail::NewEmailGrowl /// If the email is filtered AND set to !NEW then: /// Mail::NewMailNone int Filter::ApplyFilters(LView *Parent, List &Filters, List &Email) { int Status = 0; ScribeWnd *App = Filters.Length() > 0 ? Filters[0]->App : NULL; if (!App) return 0; bool Logging = App->LogFilterActivity(); LStream *LogStream = NULL; if (Logging) LogStream = App->ShowScriptingConsole(); LAutoPtr Prog; if (Parent && Prog.Reset(new LProgressDlg(Parent))) { Prog->SetRange(Email.Length()); Prog->SetDescription("Filtering..."); Prog->SetType("email"); } for (auto m: Email) { bool Act = false; bool Stop = false; m->IncRef(); for (auto f: Filters) { if (Stop) break; if (f->Test(m, Stop, LogStream)) { f->DoActions(m, Stop, LogStream); Act = true; } } if (Act) { Status++; if (m && m->NewEmail == Mail::NewEmailFilter) { m->NewEmail = Mail::NewEmailGrowl; } } else if (m && m->NewEmail == Mail::NewEmailFilter) { m->NewEmail = Mail::NewEmailBayes; } m->DecRef(); m = NULL; if (Prog) { Prog->Value(Prog->Value() + 1); if (Prog->IsCancelled()) break; } } return Status; } Filter *Filter::GetFilterAt(int Index) { ScribeFolder *f = GetFolder(); if (f) { for (auto t: f->Items) { Filter *f = t->IsFilter(); if (f && f->GetIndex() == Index) { return f; } } } return 0; } enum TermType { TermString, TermVariant }; class ExpTerm { public: TermType Type; LVariant Value; ExpTerm(TermType t) { Type = t; } }; bool Filter::Evaluate(char *str, LVariant &v) { char *BufStr = NewStr(str); char *s = BufStr; if (s) { List Terms; const char *Ws = " \t\r\n"; while (s && *s) { while (*s && strchr(Ws, *s)) s++; if (*s && *s == '\"') { char *Start = ++s; char *In = s; char *Out = s; while (*In) { if (In[0] == '\"') { if (In[1] == '\"') { // Quote *Out++ = '\"'; In += 2; } else { // End of string In++; break; } } else { *Out++ = *In++; } } *Out++ = 0; s = In; ExpTerm *t; Terms.Insert(t = new ExpTerm(TermString)); if (t) { t->Value = Start; } } else { char *Start = s; while (*s && !strchr(Ws, *s)) s++; while (*s && strchr(Ws, *s)) s++; char *Src = NewStr(Start, s-Start); if (Src) { ExpTerm *t; Terms.Insert(t = new ExpTerm(TermVariant)); if (t) { if (GetValue(Start, t->Value)) { switch (t->Value.Type) { default: break; case GV_BINARY: case GV_LIST: case GV_DOM: case GV_VOID_PTR: { v = t->Value; DeleteArray(BufStr); DeleteArray(Src); Terms.DeleteObjects(); return true; } } } else { t->Type = TermString; t->Value = Src; } } DeleteArray(Src); } } } LStringPipe Out; auto It = Terms.begin(); ExpTerm *t = *It; if (t) { if (Terms.Length() > 1) { // Collapse terms for (; t; t=*(++It)) { char Buf[128]; switch (t->Value.Type) { default: break; case GV_INT32: { sprintf_s(Buf, sizeof(Buf), "%i", t->Value.Value.Int); Out.Push(Buf); break; } case GV_INT64: { sprintf_s(Buf, sizeof(Buf), LPrintfInt64, t->Value.Value.Int64); Out.Push(Buf); break; } case GV_BOOL: { sprintf_s(Buf, sizeof(Buf), "%i", (int)t->Value.Value.Bool); Out.Push(Buf); break; } case GV_DOUBLE: { sprintf_s(Buf, sizeof(Buf), "%g", t->Value.Value.Dbl); Out.Push(Buf); break; } case GV_STRING: { if (t->Value.Str()) Out.Push(t->Value.Str()); break; } case GV_DATETIME: { t->Value.Value.Date->Get(Buf, sizeof(Buf)); Out.Push(Buf); break; } case GV_BINARY: case GV_LIST: case GV_DOM: case GV_NULL: case GV_VOID_PTR: { break; } } } v.OwnStr(Out.NewStr()); } else { v = t->Value; } } Terms.DeleteObjects(); } DeleteArray(BufStr); return true; } bool Filter::SetVariant(const char *Name, LVariant &Value, const char *Array) { ScribeDomType Field = StrToDom(Name); switch (Field) { case SdName: SetName(Value.Str()); break; case SdConditionsXml: ConditionsCache.Reset(); SetConditionsXml(Value.Str()); break; case SdActionsXml: SetActionsXml(Value.Str()); break; case SdDateModified: return SetDateField(FIELD_DATE_MODIFIED, Value); default: return false; } return true; } bool Filter::CallMethod(const char *MethodName, LVariant *ReturnValue, LArray &Args) { ScribeDomType Method = StrToDom(MethodName); switch (Method) { case SdAddCondition: // Type: (String Feild, String Op, String Value) { if (Args.Length() != 3) { LgiTrace("%s:%i - SdAddCondition: wrong number of parameters %i (expecting 3)\n", _FL, Args.Length()); break; } const char *Field = Args[0]->Str(); const char *Op = Args[1]->Str(); const char *Value = Args[2]->Str(); if (!Field || !Op || !Value) { LgiTrace("%s:%i - SdAddCondition: Missing values.\n", _FL); break; } LAutoPtr t = Parse(false); LXmlTag *Cond; if (!t || !t->IsTag(ELEMENT_CONDITIONS) || (Cond = t->Children[0]) == NULL) { LgiTrace("%s:%i - SdAddCondition: Failed to parse conditions.\n", _FL); break; } if (!Cond->IsTag(ELEMENT_AND) && !Cond->IsTag(ELEMENT_OR)) { LgiTrace("%s:%i - SdAddCondition: Unexpected root operator.\n", _FL); break; } // Check that the condition doesn't already exist... for (auto c : Cond->Children) { if (c->IsTag(ELEMENT_CONDITION)) { char *CFeild = c->GetAttr(ATTR_FIELD); char *COp = c->GetAttr(ATTR_OP); char *CVal = c->GetAttr(ATTR_VALUE); if (!Stricmp(Field, CFeild) && !Stricmp(Op, COp) && !Stricmp(Value, CVal)) { return true; } } } LXmlTag *n = new LXmlTag(ELEMENT_CONDITION); if (!n) { LgiTrace("%s:%i - SdAddCondition: Alloc failed.\n", _FL); break; } n->SetAttr(ATTR_FIELD, Field); n->SetAttr(ATTR_OP, Op); n->SetAttr(ATTR_VALUE, Value); Cond->InsertTag(n); LXmlTree tree; LStringPipe p; if (!tree.Write(t, &p)) { LgiTrace("%s:%i - SdAddCondition: Failed to write XML.\n", _FL); break; } LAutoString a(p.NewStr()); ConditionsCache.Reset(); SetConditionsXml(a); SetDirty(true); return true; } case SdAddAction: // Type: (String ActionName, String Value) { if (Args.Length() != 2) { LgiTrace("%s:%i - SdAddAction: wrong number of parameters %i (expecting 3)\n", _FL, Args.Length()); break; } LString Action = Args[0]->CastString(); if (!Action) { LgiTrace("%s:%i - SdAddAction: Missing action name.\n", _FL); break; } FilterAction a(GetObject()->GetStore()); for (ActionName *an = ActionNames; an->Id; an++) { if (Action.Equals(an->Default)) { a.Type = (FilterActionTypes) (an - ActionNames); - a.Arg1.Reset(NewStr(Args[1]->CastString())); + a.Arg1 = Args[1]->CastString(); AddAction(&a); return true; } } LgiTrace("%s:%i - SdAddAction: Action '%s' not found.\n", _FL, Action.Get()); return false; } case SdStopFiltering: // Type: () { if (d->Stop) { *d->Stop = true; return true; } else LgiTrace("%s:%i - No stop parameter to set.\n", _FL); return true; } case SdDoActions: // Type: (Mail Object) { if (Args.Length() == 1) { LDom *d = Args[0]->CastDom(); Mail *m = dynamic_cast(d); if (m) { bool Stop = false; return DoActions(m, Stop); } else LgiTrace("%s:%i - DoActions: failed to cast arg1 to Mail object.\n", _FL); } else LgiTrace("%s:%i - DoActions is expecting 1 argument, not %i.\n", _FL, Args.Length()); return true; } default: break; } return Thing::CallMethod(MethodName, ReturnValue, Args); } bool Filter::GetVariant(const char *Var, LVariant &Value, const char *Array) { ScribeDomType Fld = StrToDom(Var); switch (Fld) { case SdMail: // Type: Mail { if (!Current) return false; Value = *Current; break; } case SdScribe: // Type: ScribeWnd { Value = (LDom*)App; break; } case SdName: // Type: String { Value = GetName(); break; } case SdTestConditions: // Type: Bool { if (Current && *Current) { bool s; bool &Stop = d->Stop ? *d->Stop : s; Value = EvaluateXml(*Current, Stop, d->Log); } else return false; break; } case SdType: // Type: Int32 { Value = GetObject()->Type(); break; } case SdConditionsXml: // Type: String { Value = GetConditionsXml(); break; } case SdActionsXml: // Type: String { Value = GetActionsXml(); break; } case SdIndex: // Type: Int32 { Value = GetIndex(); break; } case SdDateModified: // Type: DateTime { return GetDateField(FIELD_DATE_MODIFIED, Value); } default: { return false; } } return true; } Thing &Filter::operator =(Thing &t) { Filter *f = t.IsFilter(); if (f) { if (GetObject() && f->GetObject()) { GetObject()->CopyProps(*f->GetObject()); } } return *this; } int Filter::Compare(LListItem *Arg, ssize_t Field) { Filter *a = dynamic_cast(Arg); if (a) { switch (Field) { case FIELD_FILTER_NAME: return a && GetName() ? _stricmp(GetName(), a->GetName()) : -1; case FIELD_FILTER_INDEX: return GetIndex() - a->GetIndex(); case FIELD_FILTER_INCOMING: return GetIncoming() - a->GetIncoming(); case FIELD_FILTER_OUTGOING: return GetOutgoing() - a->GetOutgoing(); } } return 0; } int Filter::GetImage(int Flags) { return ICON_FILTER; } void DisplayXml(LStream &s, char *xml) { char *start = xml; char *e; for (e = xml; *e; e++) { if (*e == '<') { s.Write(start, e - start); s.Print("<"); start = e + 1; } } s.Write(start, e - start); } void DescribeCondition(LStream &s, LXmlTag *t) { int i = 0; if (t->IsTag(ELEMENT_AND) || t->IsTag(ELEMENT_OR)) { for (auto c: t->Children) { LStringPipe p; DescribeCondition(p, c); LAutoString a(p.NewStr()); if (i) s.Print(" %s ", t->GetTag()); s.Print("(%s)", a.Get()); i++; } } else { // Condition char *f = t->GetAttr(ATTR_FIELD); char *v = t->GetAttr(ATTR_VALUE); int Not = t->GetAsInt(ATTR_NOT) > 0; const char *o = t->GetAttr(ATTR_OP); if (o && IsDigit(*o)) o = OpNames[atoi(o)]; s.Print("%s%s %s \"%s\"", Not ? "!" : "", f, o, v); } } LAutoString Filter::DescribeHtml() { LStringPipe p(256); p.Print("<style>\n" ".error { color: red; font-weight: bold; }\n" ".op { color: blue; }\n" ".var { color: #800; }\n" "pre { color: green; }\n" "</style>\n" "\n" "Name: %s
\n", GetName()); p.Print("Incoming: %i
\n", GetIncoming()); p.Print("Outgoing: %i
\n", GetOutgoing()); if (GetConditionsXml()) { LAutoPtr r = Parse(false); if (r && r->Children.Length()) { p.Print("Conditions:
    \n"); for (auto c: r->Children) { p.Print("
  • "); DescribeCondition(p, c); } p.Print("
\n"); } } if (GetActionsXml()) { LAutoPtr r = Parse(true); if (r && r->Children.Length()) { p.Print("Actions:
    \n"); for (auto c: r->Children) { if (!c->IsTag(ELEMENT_ACTION)) continue; LAutoPtr a(new FilterAction(GetObject()->GetStore())); if (a->Set(c)) { p.Print("
  • "); a->DescribeHtml(this, p); } } p.Print("
\n"); } } if (GetScript()) { LXmlTree t; LAutoString e(t.EncodeEntities(GetScript(), -1, "<>")); p.Print("Script:
%s
\n", e.Get()); } p.Print("\n"); return LAutoString(p.NewStr()); } void Filter::Empty() { if (GetObject()) { ConditionsCache.Reset(); SetConditionsXml(0); SetName(0); } } bool Filter::EvaluateTree(LXmlTag *n, Mail *m, bool &Stop, LStream *Log) { bool Status = false; if (n && n->GetTag() && m && m->GetObject()) { if (n->IsTag(ELEMENT_AND)) { if (Log) Log->Print("\tAnd {\n"); for (auto c: n->Children) { if (!EvaluateTree(c, m, Stop, Log)) { if (Log) Log->Print("\t} (false)\n"); return false; } } if (Log) Log->Print("\t} (true)\n"); Status = true; } else if (n->IsTag(ELEMENT_OR)) { if (Log) Log->Print("\tOr {\n"); for (auto c: n->Children) { if (EvaluateTree(c, m, Stop, Log)) { if (Log) Log->Print("\t} (true)\n"); return true; } } if (Log) Log->Print("\t} (false)\n"); } else if (n->IsTag(ELEMENT_CONDITION)) { FilterCondition *c = new FilterCondition; if (c) { if (!c->Set(n)) { LAssert(0); } else { Status = c->Test(this, m, Log); if (c->Not) Status = !Status; if (Log) { Log->Print("\tResult=%i (not=%i)\n", Status, c->Not); } } DeleteObj(c); } } else LAssert(0); } else LAssert(0); return Status; } bool FilterCondition::Set(LXmlTag *t) { if (!t) return false; Source.Reset(NewStr(t->GetAttr(ATTR_FIELD))); Value.Reset(NewStr(t->GetAttr(ATTR_VALUE))); Not = t->GetAsInt(ATTR_NOT) > 0; char *o = t->GetAttr(ATTR_OP); if (o) { if (IsDigit(*o)) Op = atoi(o); else { for (int i=0; OpNames[i]; i++) { if (!_stricmp(OpNames[i], o)) { Op = i; break; } } } } return true; } bool Filter::EvaluateXml(Mail *m, bool &Stop, LStream *Log) { bool Status = false; if (ValidStr(GetConditionsXml())) { if (!ConditionsCache) ConditionsCache = Parse(false); if (ConditionsCache && ConditionsCache->Children.Length()) Status = EvaluateTree(ConditionsCache->Children[0], m, Stop, Log); } return Status; } bool Filter::Test(Mail *m, bool &Stop, LStream *Log) { bool Status = false; if (Log) Log->Print("Filter.Test '%s':\n", GetName()); Current = &m; if (m && m->GetObject()) { d->Stop = &Stop; d->Log = Log; if (ValidStr(GetScript())) { OnFilterScript(this, m, GetScript()); } else if (ValidStr(GetConditionsXml())) { Status = EvaluateXml(m, Stop, Log); } else LAssert(0); d->Stop = 0; d->Log = 0; } Current = 0; return Status; } bool Filter::DoActions(Mail *&m, bool &Stop, LStream *Log) { if (!App || !m) return false; Current = &m; LAutoPtr r = Parse(true); if (r) { LArray Act; for (auto c: r->Children) { if (c->IsTag(ELEMENT_ACTION)) { FilterAction *a = new FilterAction(GetObject()->GetStore()); if (a) { if (a->Set(c)) Act.Add(a); else LAssert(!"Can't convert xml to action."); } } } for (unsigned i=0; iGetObject(); i++) { FilterAction *a = Act[i]; a->Do(this, App, m, Log); } Act.DeleteObjects(); } Current = 0; if (GetStopFiltering()) { Stop = true; } return true; } ThingUi *Filter::DoUI(MailContainer *c) { if (!Ui) { MaxIndex = MAX(MaxIndex, GetIndex()); if (GetIndex() < 0) { SetIndex(++MaxIndex); } Ui = new FilterUi(this); } #if WINNATIVE if (Ui) SetForegroundWindow(Ui->Handle()); #endif return Ui; } LAutoPtr Filter::Parse(bool Actions) { LAutoPtr Ret; auto RawXml = Actions ? GetActionsXml() : GetConditionsXml(); if (!RawXml) return Ret; LMemStream Xml(RawXml, strlen(RawXml), false); LXmlTree t; Ret.Reset(new LXmlTag); if (!t.Read(Ret, &Xml)) Ret.Reset(); return Ret; } void Filter::AddAction(FilterAction *Action) { if (!Action) return; LAutoPtr a = Parse(true); if (!a) a.Reset(new LXmlTag("Actions")); LAutoPtr n(new LXmlTag(ELEMENT_ACTION)); if (!Action->Get(n)) return; a->InsertTag(n.Release()); LXmlTree t; LStringPipe p; if (!t.Write(a, &p)) return; LAutoString Xml(p.NewStr()); SetActionsXml(Xml); SetDirty(true); } bool Filter::Save(ScribeFolder *Into) { bool Status = false; if (!Into) { Into = GetFolder(); if (!Into) { Into = App->GetFolder(FOLDER_FILTERS); } } if (Into) { SetParentFolder(Into); if (ChkIncoming) SetIncoming(ChkIncoming->Value()!=0); if (ChkOutgoing) SetOutgoing(ChkOutgoing->Value()!=0); if (ChkInternal) SetInternal(ChkInternal->Value()!=0); LDateTime Now; GetObject()->SetDate(FIELD_DATE_MODIFIED, &Now.SetNow()); Store3Status s = Into->WriteThing(this); Status = s != Store3Error; if (Status) SetDirty(false); } return Status; } void Filter::OnPaint(ItemPaintCtx &Ctx) { LListItem::OnPaint(Ctx); } void Filter::OnMouseClick(LMouse &m) { LListItem::OnMouseClick(m); if (m.Double()) { // open the UI for the Item DoUI(); } else if (m.Right()) { // open the right click menu auto RClick = new LSubMenu; if (RClick) { RClick->AppendItem(LLoadString(IDS_OPEN), IDM_OPEN); RClick->AppendItem(LLoadString(IDS_DELETE), IDM_DELETE); RClick->AppendItem(LLoadString(IDS_EXPORT), IDM_EXPORT); if (Parent->GetMouse(m, true)) { switch (RClick->Float(Parent, m.x, m.y)) { case IDM_OPEN: { DoUI(); break; } case IDM_DELETE: { LVariant ConfirmDelete; App->GetOptions()->GetValue(OPT_ConfirmDelete, ConfirmDelete); if (!ConfirmDelete.CastInt32() || LgiMsg(GetList(), LLoadString(IDS_DELETE_ASK), AppName, MB_YESNO) == IDYES) { List Del; LList *ParentList = LListItem::Parent; if (ParentList && ParentList->GetSelection(Del)) { for (auto m: Del) { auto f = dynamic_cast(m); if (f) f->OnDelete(); } } } break; } case IDM_EXPORT: { ExportAll(GetList(), sTextXml, NULL); break; } } } DeleteObj(RClick); } } } int *Filter::GetDefaultFields() { static int Def[] = { FIELD_FILTER_NAME, 0, 0, 0 }; return Def; } const char *Filter::GetFieldText(int Field) { switch (Field) { case FIELD_FILTER_NAME: return GetName(); case FIELD_FILTER_CONDITIONS_XML: return GetConditionsXml(); case FIELD_FILTER_ACTIONS_XML: return GetActionsXml(); case FIELD_FILTER_SCRIPT: return GetScript(); } return NULL; } void Filter::OnColumnNotify(int Col, int64 Data) { if (!IgnoreCheckEvents) { switch (Col) { case 2: case 3: case 4: { SetDirty(true); break; } } } } const char *Filter::GetText(int i) { int Field = 0; if (FieldArray.Length()) { if (i >= 0 && i < (int)FieldArray.Length()) Field = FieldArray[i]; } else if (i >= 0 && i < CountOf(DefaultFilterFields)) { Field = DefaultFilterFields[i]; } switch (Field) { case FIELD_FILTER_NAME: { return GetName(); break; } case FIELD_FILTER_INDEX: { static char i[16]; sprintf_s(i, sizeof(i), "%i", GetIndex()); return i; break; } } return 0; } int FilterIndexCmp(Filter **a, Filter **b) { int ai = (*a)->GetIndex(); int bi = (*b)->GetIndex(); return ai - bi; } void Filter::Reindex(ScribeFolder *Folder) { if (!Folder) return; LArray Zero; LArray Sort; for (auto t : Folder->Items) { Filter *f = t->IsFilter(); if (f) { int Idx = f->GetIndex(); if (Idx > 0) { Sort.Add(f); } else { Zero.Add(f); } } } Sort.Sort(FilterIndexCmp); LArray Map; for (unsigned n=0; nGetIndex(); if (Idx != i + 1) { Sort[i]->SetIndex(i + 1); Sort[i]->SetDirty(); Sort[i]->Update(); Changed = true; } } if (Changed) Folder->ReSort(); } ////////////////////////////////////////////////////////// enum ConditionOptions { FIELD_ANYWHERE = FIELD_MAX }; int FilterCallback( LFilterView *View, LFilterItem *Item, GFilterMenu Menu, LRect &r, LArray *GetList, void *Data) { int Status = -1; if (!View) return Status; switch (Menu) { case FMENU_FIELD: { LSubMenu s; LString::Array Names; ItemFieldDef *FieldDefs = MailFieldDefs; for (ItemFieldDef *i = FieldDefs; i->DisplayText; i++) { const char *Trans = LLoadString(i->FieldId); auto idx = (i - FieldDefs) + 1; Names[idx] = DomToStr(i->Dom); s.AppendItem(Trans ? Trans : i->DisplayText, (int)idx, true); } s.AppendItem(LLoadString(IDS_ATTACHMENTS_DATA), FIELD_ATTACHMENTS_DATA, true); s.AppendItem(LLoadString(IDS_ATTACHMENTS_NAME), FIELD_ATTACHMENTS_NAME, true); s.AppendItem(LLoadString(IDS_MEMBER_OF_GROUP), FIELD_MEMBER_OF_GROUP, true); s.AppendItem(LLoadString(IDS_ANYWHERE), FIELD_ANYWHERE, true); LPoint p(r.x1, r.y2 + 1); View->PointToScreen(p); int Cmd = s.Float(View, p.x, p.y, true); switch (Cmd) { case FIELD_ATTACHMENTS_DATA: Item->SetField("mail.Attachments"); break; case FIELD_ATTACHMENTS_NAME: Item->SetField("mail.AttachmentNames"); break; case FIELD_MEMBER_OF_GROUP: Item->SetField("mail.From.Groups"); break; case FIELD_ANYWHERE: Item->SetField("mail.*"); break; default: if (Cmd > 0 && Cmd < Names.Length()) Item->SetField(LString("mail.") + Names[Cmd]); break; } break; } case FMENU_OP: { if (GetList) { for (const char **o = GetOpNames(true); *o; o++) { GetList->Add(NewStr(*o)); } Status = true; } /* else { auto s = new LSubMenu; if (s) { int n = 1; for (char **o = GetOpNames(true); *o; o++) { s->AppendItem(*o, n++, true); } LPoint p(r.x1, r.y2 + 1); View->PointToScreen(p); int Cmd = s->Float(View, p.x, p.y, true); if (Cmd > 0) { Item->SetOp(OpNames[Cmd - 1]); } DeleteObj(s); } } */ break; } case FMENU_VALUE: { break; } } return Status; } ////////////////////////////////////////////////////////// struct FilterUiPriv { }; FilterUi::FilterUi(Filter *item) : ThingUi(item, "Filter") { d = new FilterUiPriv; Item = item; if (!(Item && Item->App)) { return; } Script = 0; Tab = 0; Actions = 0; Conditions = 0; LRect r(100, 100, 800, 600); SetPos(r); MoveSameScreen(item->App); // Create window #if WINNATIVE CreateClassW32("FilterUi", LoadIcon(LProcessInst(), MAKEINTRESOURCE(IDI_FILTER))); #endif if (Attach(0)) { // Setup UI Commands.Toolbar = Item->App->LoadToolbar(this, Item->App->GetResourceFile(ResToolbarFile), Item->App->GetToolbarImgList()); if (Commands.Toolbar) { Commands.Toolbar->Attach(this); Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_SAVE)), IDM_SAVE, TBT_PUSH, true, IMG_SAVE); Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_SAVE_CLOSE)), IDM_SAVE_CLOSE, TBT_PUSH, true, IMG_SAVE_AND_CLOSE); Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_DELETE)), IDM_DELETE, TBT_PUSH, true, IMG_TRASH); Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_HELP)), IDM_HELP, TBT_PUSH, true, IMG_HELP); Commands.SetupCallbacks(GetItem()->App, this, GetItem(), LThingUiToolbar); } LTabPage *Cond = 0; LTabPage *Act = 0; Tab = new LTabView(91, 0, 0, 1000, 1000, 0); if (Tab) { Tab->Attach(this); Tab->SetPourChildren(true); LTabPage *Filter = Tab->Append(LLoadString(IDS_FILTER)); if (Filter) { #ifdef _DEBUG auto Status = #endif Filter->LoadFromResource(IDD_FILTER); LAssert(Status); Name(Filter->Name()); } Cond = Tab->Append(LLoadString(IDS_CONDITIONS)); if (Cond) { Conditions = new LFilterView(FilterCallback, Item); if (Conditions) { Cond->Append(Conditions); Conditions->SetPourLargest(true); } } Act = Tab->Append(LLoadString(IDS_ACTIONS)); if (Act) { #ifdef _DEBUG auto Status = #endif Act->LoadFromResource(IDD_FILTER_ACTION); LAssert(Status); if (GetViewById(IDC_FILTER_ACTIONS, Actions)) Actions->MultiSelect(false); } LTabPage *ScriptTab = Tab->Append(""); if (ScriptTab && ScriptTab->LoadFromResource(IDD_FILTER_SCRIPT)) { if (GetViewById(IDC_SCRIPT, Script)) { Script->SetWrapType(TEXTED_WRAP_NONE); Script->Sunken(true); Script->SetPourLargest(true); } else LAssert(0); } } // Show window Visible(true); if (Cond && Item) { LCombo *Cbo; if (GetViewById(IDC_ACTION, Cbo)) { for (ActionName *o = ActionNames; o->Id; o++) { const char *s = LLoadString(o->Id, o->Default); Cbo->Insert(s); } } } OnLoad(); } RegisterHook(this, LKeyEvents); } FilterUi::~FilterUi() { if (Item) Item->Ui = 0; DeleteObj(d); } bool FilterUi::OnViewKey(LView *v, LKey &k) { if (k.CtrlCmd()) { switch (k.c16) { case 's': case 'S': { if (k.Down()) OnSave(); return true; } case 'w': case 'W': { if (k.Down()) { OnSave(); Quit(); } return true; } } } return false; } int FilterUi::OnNotify(LViewI *Col, LNotification n) { int Reindex = 0; int InsertOffset = 0; switch (Col->GetId()) { case IDC_TYPE_CBO: case IDC_ARG_EDIT: { if (Actions) { List Sel; if (Actions->GetSelection(Sel)) { for (auto a: Sel) a->OnNotify(Col, n); } } break; } case IDC_BROWSE_ARG: { if (Actions) { List Sel; if (Actions->GetSelection(Sel)) { FilterAction *a = Sel[0]; if (a) { a->Browse(Item->App, this); a->OnNotify(Col, n); } } } break; } case IDC_UP: InsertOffset = -1; // fall thru case IDC_DOWN: { if (!InsertOffset) InsertOffset = 1; if (!Actions) break; List Items; if (!Actions->GetSelection(Items) && Items[0]) break; int Idx = Actions->IndexOf(Items[0]) + InsertOffset; FilterAction *Last = 0; for (auto a: Items) { Actions->Remove(a); Actions->Insert(a, Idx++); Last = a; } if (Last) { Actions->Focus(true); Last->Select(true); } break; } case IDC_NEW_FILTER_ACTION: { if (Actions) { FilterAction *n = new FilterAction(Item->GetObject()->GetStore()); Actions->Insert(n); Actions->Select(n); Actions->Focus(true); } break; } case IDC_DELETE_FILTER_ACTION: { if (Actions) { List Items; if (Actions->GetSelection(Items) && Items[0]) { int Idx = Actions->IndexOf(Items[0]); Items.DeleteObjects(); if (Idx >= (int)Actions->Length()) Idx = (int)Actions->Length() - 1; Actions->Select(Actions->ItemAt(Idx)); Actions->Focus(true); } } break; } case IDC_LAUNCH_HELP: { switch (Tab->Value()) { case 0: // Name/Index default: { Item->App->LaunchHelp("filters.html"); break; } case 1: // Conditions { Item->App->LaunchHelp("filters.html#cond"); break; } case 2: // Actions { Item->App->LaunchHelp("filters.html#actions"); break; } case 3: // Script { Item->App->LaunchHelp("filters.html#script"); break; } } break; } case IDC_FILTER_UP: { Reindex = 1; break; } case IDC_FILTER_DOWN: { Reindex = -1; break; } } if (Reindex) { // Remove holes in the indexing ScribeFolder *Folder = Item->GetFolder(); if (Folder) { Folder->ReSort(); } // Swap entries int i = (int)GetCtrlValue(IDC_FILTER_INDEX); if (i >= 0) { Filter *f = Item->GetFilterAt(i - Reindex); if (f) { int n = f->GetIndex(); f->SetIndex(Item->GetIndex()); f->SetDirty(); Item->SetIndex(n); Item->SetDirty(); f->Save(); Item->Save(); SetCtrlValue(IDC_FILTER_INDEX, Item->GetIndex()); Item->App->GetItemList()->ReSort(); Item->Update(); f->Update(); } } } return 0; } LMessage::Result FilterUi::OnEvent(LMessage *Msg) { switch (Msg->Msg()) { #ifdef WIN32 case WM_CLOSE: { Quit(); return 0; } #endif } return LWindow::OnEvent(Msg); } void LoadTree(LFilterView *v, LXmlTag *t, LTreeNode *i) { int idx = 0; for (auto c: t->Children) { if (c->GetTag()) { LFilterItem *n = 0; bool Cond = false; if (c->IsTag(ELEMENT_AND)) n = v->Create(LNODE_AND); else if (c->IsTag(ELEMENT_OR)) n = v->Create(LNODE_OR); else if (c->IsTag(ELEMENT_CONDITION)) { Cond = true; n = v->Create(LNODE_COND); } if (n) { if (Cond) { n->SetNot(c->GetAsInt(ATTR_NOT) > 0); n->SetField(c->GetAttr(ATTR_FIELD)); n->SetValue(c->GetAttr(ATTR_VALUE)); char *o = c->GetAttr(ATTR_OP); if (o) { if (IsDigit(*o)) n->SetOp(atoi(o)); else { for (int i=0; OpNames[i]; i++) { if (!_stricmp(OpNames[i], o)) { n->SetOp(i); break; } } } } } i->Insert(n, idx++); LoadTree(v, c, n); } } } } void FilterUi::OnLoad() { if (Item) { LAutoPtr r = Item->Parse(true); if (r && Actions) { for (auto c: r->Children) { FilterAction *a = new FilterAction(Item->GetObject()->GetStore()); if (a) { if (a->Set(c)) { Actions->Insert(a); } else LAssert(!"Can't convert xml to action."); } } } auto FilterName = Item->GetName(); SetCtrlName(IDC_NAME, FilterName); SetCtrlValue(IDC_FILTER_INDEX, Item->GetIndex()); SetCtrlName(IDC_SCRIPT, Item->GetScript()); SetCtrlValue(IDC_STOP_FILTERING, Item->GetStopFiltering()); SetCtrlValue(IDC_INCOMING, Item->GetIncoming()); SetCtrlValue(IDC_OUTGOING, Item->GetOutgoing()); SetCtrlValue(IDC_INTERNAL_FILTERING, Item->GetInternal()); auto Xml = Item->GetConditionsXml(); if (Conditions && Xml) { LAutoPtr x(new LXmlTag); if (x) { LMemStream p(Xml, strlen(Xml)); LXmlTree t; if (t.Read(x, &p, 0)) { Conditions->Empty(); LoadTree(Conditions, x, Conditions->GetRootNode()); if (!Conditions->GetRootNode()->GetChild()) { Conditions->SetDefault(); } } } } if (ValidStr(FilterName)) { LString s; s.Printf("%s - %s", LLoadString(IDS_FILTER), FilterName); Name(s); } } } void SaveTree(LXmlTag *t, LTreeNode *i) { for (LTreeNode *c = i->GetChild(); c; c = c->GetNext()) { LFilterItem *fi = dynamic_cast(c); if (fi) { const char *Tag = 0; bool Cond = false; switch (fi->GetNode()) { default: break; case LNODE_AND: Tag = ELEMENT_AND; break; case LNODE_OR: Tag = ELEMENT_OR; break; case LNODE_COND: Cond = true; Tag = ELEMENT_CONDITION; break; } if (Tag) { LXmlTag *n = new LXmlTag(Tag); if (n) { if (Cond) { n->SetAttr(ATTR_NOT, fi->GetNot()); n->SetAttr(ATTR_FIELD, fi->GetField()); n->SetAttr(ATTR_OP, fi->GetOp()); n->SetAttr(ATTR_VALUE, fi->GetValue()); } t->InsertTag(n); SaveTree(n, fi); } } } } } void FilterUi::OnSave() { if (Item) { Item->SetName(GetCtrlName(IDC_NAME)); Item->SetScript(GetCtrlName(IDC_SCRIPT)); Item->SetStopFiltering(GetCtrlValue(IDC_STOP_FILTERING)!=0); Item->SetIncoming(GetCtrlValue(IDC_INCOMING)!=0); Item->SetOutgoing(GetCtrlValue(IDC_OUTGOING)!=0); Item->ChkIncoming->Value(GetCtrlValue(IDC_INCOMING)); Item->ChkOutgoing->Value(GetCtrlValue(IDC_OUTGOING)); Item->ChkInternal->Value(GetCtrlValue(IDC_INTERNAL_FILTERING)); if (Conditions) { LXmlTag *x = new LXmlTag(ELEMENT_CONDITIONS); if (x) { SaveTree(x, Conditions->GetRootNode()); LXmlTree t; LStringPipe p; if (t.Write(x, &p)) { LAutoString a(p.NewStr()); Item->ConditionsCache.Reset(); Item->SetConditionsXml(a); } DeleteObj(x); } } LXmlTag x("Actions"); List Act; Actions->GetAll(Act); for (size_t i=0; i c(new LXmlTag("Action")); if (a->Get(c)) { x.InsertTag(c.Release()); } } LXmlTree t; LStringPipe p; t.Write(&x, &p); LAutoString s(p.NewStr()); Item->SetActionsXml(s); if (Item->Save()) { Item->Reindex(Item->GetFolder()); } } } int FilterUi::OnCommand(int Cmd, int Event, OsView Window) { switch (Cmd) { case IDM_SAVE: { OnSave(); break; } case IDM_SAVE_CLOSE: { OnSave(); // fall thru } case IDM_CLOSE: { Quit(); break; } case IDM_DELETE: { Item->Ui = 0; Item->OnDelete(); Item = 0; Quit(); break; } case IDM_HELP: { App->LaunchHelp("filters.html"); break; } case IDC_NEW_FILTER_ACTION: { LList *l; if (GetViewById(IDC_FILTER_ACTIONS, l)) { } break; } case IDC_DELETE_FILTER_ACTION: { break; } default: { Commands.ExecuteCallbacks(GetItem()->App, this, GetItem(), Cmd); break; } } return 0; } 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, "") { 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, "/"); + auto Path = LString(InitSel).SplitDelimit("/"); 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/ScribeGroup.cpp b/Code/ScribeGroup.cpp --- a/Code/ScribeGroup.cpp +++ b/Code/ScribeGroup.cpp @@ -1,1447 +1,1447 @@ #include "Scribe.h" #include "lgi/common/TextView3.h" #include "resdefs.h" #include "ScribeListAddr.h" #include "lgi/common/DisplayString.h" #include "lgi/common/LgiRes.h" #include "lgi/common/FileSelect.h" #include "AddressSelect.h" ItemFieldDef GroupFieldDefs[] = { {ContactGroupName, SdName, GV_STRING, FIELD_GROUP_NAME, IDC_GROUP_NAME}, {ContactGroupList, SdList, GV_STRING, FIELD_GROUP_LIST, IDC_GROUP_LIST}, {ContactGroupDateModified, SdDateModified, GV_DATETIME, FIELD_DATE_MODIFIED, 0}, {0} }; int DefaultGroupFields[] = { FIELD_GROUP_NAME, FIELD_DATE_MODIFIED, 0, 0 }; //////////////////////////////////////////////////////////////////////////////////////// class LAddressEdit; struct AddressMeta { int Id; bool Referenced; LAddressEdit *Edit; ListAddr Addr; LString Text; AddressMeta(LAddressEdit *edit, int id = -1); ~AddressMeta(); bool Matched() { return Addr.Length() == 1; } void Ins(); bool IsResolved(); bool IsUnique(); void OpenContact(); }; class LAddressEdit : public LTextView3 { friend struct AddressMeta; LHashTbl,AddressMeta*> Map; LArray Items; int CreateId() { int Id; while (Map.Find(Id = LRand(1000))) ; return Id; } AddressMeta *GetMeta(LStyle &s) { return Map.Find(s.Data.CastInt32()); } bool UpdateMeta(LStyle &Style) { auto m = GetMeta(Style); if (!m) return false; m->Addr.SetText(m->Text); // LgiTrace("UpdateMeta '%s' -> %i\n", m->Text.Get(), m->Addr.Length()); if (m->Addr.Length() > 1) { // Matched more than one recip Style.Fore.Rgb(255, 0, 0); Style.Font = Underline; } else if (m->Addr.Length()) { // Matched a single recip Style.Fore = LColour(L_BLACK); Style.Font = Underline; } else { // No match Style.Fore.Rgb(0xb0, 0xb0, 0xb0); Style.Font = GetFont(); Style.Decor = LCss::TextDecorSquiggle; Style.DecorColour = LColour::Red; } return true; } public: bool AllowPour; ScribeWnd *App; LAddressEdit(LRect *p, const char *t); void OnPaint(LSurface *pDC); void PourStyle(size_t Start, ssize_t Length); bool Insert(size_t At, const char16 *Data, ssize_t Len); bool Delete(size_t At, ssize_t Len); bool GetStyles(List &Styles); int WillAccept(LDragFormats &Formats, LPoint Pt, int KeyState); int OnDrop(LArray &Data, LPoint Pt, int KeyState); bool OnStyleClick(LStyle *style, LMouse *m); bool OnStyleMenu(LStyle *style, LSubMenu *m); void OnStyleMenuClick(LStyle *style, int i); }; class GroupUi : public ThingUi, public LResourceLoad { ContactGroup *Item; LAddressEdit *Edit; public: GroupUi(ContactGroup *item); ~GroupUi(); int OnNotify(LViewI *c, LNotification n); void ResolveAll(); void OnDirty(bool Dirty) {} void OnLoad(); void OnSave(); int WillAccept(LDragFormats &Formats, LPoint Pt, int KeyState); int OnDrop(LArray &Data, LPoint Pt, int KeyState); void Add(char *Email); }; //////////////////////////////////////////////////////////////////////////////////////// class ContactGroupPrivate { public: }; ContactGroup::ContactGroup(ScribeWnd *app, LDataI *object) : Thing(app, object) { DefaultObject(object); d = new ContactGroupPrivate; Ui = 0; } ContactGroup::~ContactGroup() { DeleteObj(d); } Thing &ContactGroup::operator =(Thing &c) { LAssert(0); return *this; } char *ContactGroup::GetDropFileName() { if (!DropFileName) { auto Name = GetName(); DropFileName.Reset(MakeFileName(Name ? Name : "group", "xml")); } return DropFileName; } bool ContactGroup::GetDropFiles(LString::Array &Files) { auto fn = GetDropFileName(); if (!fn) return false; if (!LFileExists(fn)) { LAutoPtr F(new LFile); if (F->Open(fn, O_WRITE)) { F->SetSize(0); Export(AutoCast(F), sMimeXml); } } if (!LFileExists(DropFileName)) return false; Files.Add(DropFileName.Get()); return true; } bool ContactGroup::GetFormats(bool Export, LString::Array &MimeTypes) { MimeTypes.Add(sTextXml); return MimeTypes.Length() > 0; } Thing::IoProgress ContactGroup::Import(IoProgressImplArgs) { if (Stricmp(mimeType, sTextXml) && Stricmp(mimeType, sMimeXml)) IoProgressNotImpl(); LXmlTree Tree; LXmlTag r, *t; if (!Tree.Read(&r, stream)) IoProgressError("Xml parse error."); if (!r.IsTag(ContactGroupObj)) IoProgressError("No ContactGroup tag."); if ((t = r.GetChildTag(ContactGroupName))) GetObject()->SetStr(FIELD_GROUP_NAME, t->GetContent()); else IoProgressError("No Name tag."); if ((t = r.GetChildTag(ContactGroupList))) GetObject()->SetStr(FIELD_GROUP_LIST, t->GetContent()); else IoProgressError("No List tag."); if ((t = r.GetChildTag(ContactGroupDateModified))) { LDateTime dt; if (dt.Set(t->GetContent())) GetObject()->SetDate(FIELD_DATE_MODIFIED, &dt); } IoProgressSuccess(); } Thing::IoProgress ContactGroup::Export(IoProgressImplArgs) { if (Stricmp(mimeType, sMimeXml)) IoProgressNotImpl(); auto Name = GetName(); LVariant Addr; GetVariant(ContactGroupList, Addr); auto Modified = GetObject()->GetDate(FIELD_DATE_MODIFIED); LXmlTag r(ContactGroupObj), *t; if ((t = r.CreateTag(ContactGroupName))) t->SetContent(Name); if ((t = r.CreateTag(ContactGroupList))) t->SetContent(Addr.Str()); if (Modified && Modified->IsValid() && (t = r.CreateTag(ContactGroupDateModified))) t->SetContent(Modified->Get()); LXmlTree tree; if (tree.Write(&r, stream)) IoProgressError("Failed to write xml."); IoProgressSuccess(); } LString::Array ContactGroup::GetAddresses() { LString::Array Addrs; LVariant l; if (GetVariant(ContactGroupList, l)) { - LToken t(l.Str()); + auto t = l.LStr().SplitDelimit(); for (unsigned i=0; i &a) { bool Status = false; LVariant l; if (GetVariant(ContactGroupList, l)) { - LToken t(l.Str()); + auto t = l.LStr().SplitDelimit(); for (unsigned i=0; iSetStr(FIELD_GROUP_NAME, Value.Str()); break; } case SdList: // Type: String[] { if (Array) { auto Idx = Atoi(Array); auto t = LString(GetObject()->GetStr(FIELD_GROUP_LIST)).SplitDelimit(ListDelimiters); if (t.IdxCheck(Idx)) t[Idx] = Value.Str(); else t.New() = Value.Str(); GetObject()->SetStr(FIELD_GROUP_LIST, LString("\n").Join(t)); } else { GetObject()->SetStr(FIELD_GROUP_LIST, Value.Str()); } break; } case SdDateModified: { return SetDateField(FIELD_DATE_MODIFIED, Value); } default: { return false; } } return true; } bool ContactGroup::GetVariant(const char *Name, LVariant &Value, const char *Array) { ScribeDomType Fld = StrToDom(Name); switch (Fld) { case SdName: // Type: String { Value = GetObject()->GetStr(FIELD_GROUP_NAME); break; } case SdList: // Type: String[] { if (Array) { - LToken t(GetObject()->GetStr(FIELD_GROUP_LIST), ", \r\n"); + auto t = LString(GetObject()->GetStr(FIELD_GROUP_LIST)).SplitDelimit(", \r\n"); int Idx = atoi(Array); if (Idx >= 0 && Idx < (int)t.Length()) { Value = t[Idx]; } } else { Value = GetObject()->GetStr(FIELD_GROUP_LIST); } break; } case SdType: // Type: Int32 { Value = GetObject()->Type(); break; } case SdDateModified: { return GetDateField(FIELD_DATE_MODIFIED, Value); } case SdUsedTs: { Value = &UsedTs; break; } default: { return false; } } return true; } bool ContactGroup::CallMethod(const char *MethodName, LVariant *ReturnValue, LArray &Args) { if (GetObject() && !Stricmp((char*)MethodName, (char*)"AddAddress")) { int Added = 0; LString Addrs = GetObject()->GetStr(FIELD_GROUP_LIST); LString::Array a = Addrs.SplitDelimit(WhiteSpace); for (unsigned i=0; iStr(); if (AddrToAdd) { bool HasAddr = false; for (unsigned n=0; nSetStr(FIELD_GROUP_LIST, Lst); SetDirty(); UsedTs.SetNow(); if (ReturnValue) *ReturnValue = true; } else if (ReturnValue) { *ReturnValue = false; } return true; } return Thing::CallMethod(MethodName, ReturnValue, Args); } bool ConvertList(ContactGroup *g, LArray &a) { List Addrs; if (g->GetAddresses(Addrs)) { for (auto e: Addrs) { ListAddr *la = new ListAddr(g->App, e, (char*)0); if (la) { la->OnFind(); a.Add(la); } } Addrs.DeleteArrays(); } return a.Length() > 0; } void ContactGroup::OnMouseClick(LMouse &m) { LListItem::OnMouseClick(m); if (m.IsContextMenu()) { // open the right click menu LScriptUi s(new LSubMenu); if (s.Sub) { List Templates; s.Sub->AppendItem(LLoadString(IDS_OPEN), IDM_OPEN, true); s.Sub->AppendItem(LLoadString(IDS_DELETE), IDM_DELETE, true); ScribeFolder *f = App->GetFolder(FOLDER_TEMPLATES); if (f) { auto Merge = s.Sub->AppendSub(LLoadString(IDS_MERGE_TEMPLATE)); if (Merge) { int n = 0; for (auto t: f->Items) { Mail *m = t->IsMail(); if (m) { Templates.Insert(m); Merge->AppendItem(m->GetSubject()?m->GetSubject():(char*)"(no subject)", IDM_MERGE_TEMPLATE_BASE+n++, true); } } } } else { s.Sub->AppendItem(LLoadString(IDS_MERGE_TEMPLATE), 0, false); } s.Sub->AppendItem(LLoadString(IDS_MERGE_FILE), IDM_MERGE_FILE, true); if (GetList()->GetMouse(m, true)) { LArray Callbacks; if (App->GetScriptCallbacks(LThingContextMenu, Callbacks)) { LScriptArguments Args(NULL); Args[0] = new LVariant(App); Args[1] = new LVariant(this); Args[2] = new LVariant(&s); for (unsigned i=0; iExecuteScriptCallback(*Callbacks[i], Args); } } int Msg; switch (Msg = s.Sub->Float(GetList(), m.x, m.y)) { case IDM_OPEN: { DoUI(); break; } case IDM_DELETE: { LVariant ConfirmDelete; App->GetOptions()->GetValue(OPT_ConfirmDelete, ConfirmDelete); if (!ConfirmDelete.CastInt32() || LgiMsg(GetList(), LLoadString(IDS_DELETE_ASK), AppName, MB_YESNO) == IDYES) { List Del; LList *ParentList = LListItem::Parent; if (ParentList && ParentList->GetSelection(Del)) { for (auto i: Del) { auto obj = dynamic_cast(i); if (obj) obj->OnDelete(); else LAssert(!"What type of object is this?"); } } } break; } case IDM_MERGE_FILE: { auto s = new LFileSelect(App); s->Type("Email Template", "*.txt;*.eml"); s->Open([this](auto dlg, auto status) { if (status) { LArray Recip; if (ConvertList(this, Recip)) App->MailMerge(Recip, dlg->Name(), 0); Recip.DeleteObjects(); } delete dlg; }); break; } default: { if (Msg >= IDM_MERGE_TEMPLATE_BASE && Msg - IDM_MERGE_TEMPLATE_BASE < (ssize_t)Templates.Length()) { Mail *Template = Templates[Msg - IDM_MERGE_TEMPLATE_BASE]; if (Template) { LArray Recip; if (ConvertList(this, Recip)) App->MailMerge(Recip, 0, Template); Recip.DeleteObjects(); } } else { // Handle any installed callbacks for menu items for (unsigned i=0; iExecuteScriptCallback(Cb, Args); } } } break; } } } DeleteObj(s.Sub); } } else if (m.Down() && m.Double()) { if (!Ui) { Ui = new GroupUi(this); } } } void ContactGroup::OnSerialize(bool Write) { if (Write) { UsedTs.SetNow(); } else if (!UsedTs.IsValid()) { auto m = GetObject()->GetDate(FIELD_DATE_MODIFIED); if (m) UsedTs = *m; } } ThingUi *ContactGroup::DoUI(MailContainer *c) { if (!Ui) Ui = new GroupUi(this); else Ui->Visible(true); return Ui; } int ContactGroup::Compare(LListItem *Arg, ssize_t Field) { ContactGroup *cg = dynamic_cast(Arg); if (!cg) return 0; switch (Field) { case FIELD_DATE_MODIFIED: { auto a = GetObject()->GetDate((int)Field); auto b = cg->GetObject()->GetDate((int)Field); if (a && b) return a->Compare(b); break; } default: { auto a = GetFieldText((int)Field); auto b = cg->GetFieldText((int)Field); if (a && b) return _stricmp(a, b); break; } } return 0; } bool ContactGroup::Save(ScribeFolder *Into) { if (!GetFolder()) { if (Into) SetParentFolder(Into); else if (App) SetParentFolder(App->GetFolder(FOLDER_GROUPS)); } if (!GetFolder()) return false; LDateTime Now; GetObject()->SetDate(FIELD_DATE_MODIFIED, &Now.SetNow()); auto Status = GetFolder()->WriteThing(this) != Store3Error; if (Status) SetDirty(false); return Status; } int *ContactGroup::GetDefaultFields() { return DefaultGroupFields; } const char *ContactGroup::GetFieldText(int Field) { switch (Field) { case FIELD_DATE_MODIFIED: { auto d = GetObject()->GetDate(Field); if (d) DateCache = d->Local().Get(); else DateCache = LLoadString(IDS_NONE); return DateCache; } } return GetObject() ? GetObject()->GetStr(Field) : 0; } const char *ContactGroup::GetText(int i) { if (FieldArray.Length()) { if (i >= 0 && i < (int)FieldArray.Length()) return GetFieldText(FieldArray[i]); } else if (i < CountOf(DefaultGroupFields)) { return GetFieldText(DefaultGroupFields[i]); } return 0; } //////////////////////////////////////////////////////////////////////////////////////// GroupUi::GroupUi(ContactGroup *item) : ThingUi(item, "Contact Group") { Item = item; Edit = 0; LRect p; LAutoString n; if (LoadFromResource(IDD_GROUP, this, &p, &n)) { SetPos(p); Name(n); MoveSameScreen(App); if (GetViewById(IDC_GROUP_LIST, Edit)) { Edit->App = App; } if (Attach(0)) { AttachChildren(); OnLoad(); Visible(true); SetWindow(this); } } } GroupUi::~GroupUi() { Item->Ui = 0; } extern bool SerializeUi(ItemFieldDef *Defs, LDataI *Object, LViewI *View, bool ToUi); void GroupUi::OnLoad() { SerializeUi(GroupFieldDefs, Item->GetObject(), this, true); auto GrpName = Item->GetObject()->GetStr(FIELD_GROUP_NAME); if (ValidStr(GrpName)) { LString s; s.Printf("%s - %s", LLoadString(IDD_GROUP), GrpName); Name(s); } } void GroupUi::ResolveAll() { // Turn all the references into email addresses List Styles; if (Edit && Edit->GetStyles(Styles)) { for (auto a: Styles) { if (!a->IsResolved() && a->IsUnique()) { a->Ins(); } } } } void GroupUi::OnSave() { ResolveAll(); // Save the group of contacts Item->SetDirty(); SerializeUi(GroupFieldDefs, Item->GetObject(), this, false); Item->Save(); Item->Update(); } int GroupUi::OnNotify(LViewI *c, LNotification n) { switch (c->GetId()) { case IDOK: { OnSave(); Quit(); break; } } return 0; } int GroupUi::WillAccept(LDragFormats &Formats, LPoint Pt, int KeyState) { Formats.Supports(ScribeThingList); Formats.SupportsFileDrops(); return Formats.GetSupported().Length() ? DROPEFFECT_COPY : DROPEFFECT_NONE; } void GroupUi::Add(char *Email) { ResolveAll(); if (Edit) { List Styles; Edit->GetStyles(Styles); for (auto a: Styles) { if (a->Addr.sAddr && _stricmp(a->Addr.sAddr, Email) == 0) { LgiTrace("%s:%i - '%s' already in group\n", _FL, Email); return; } } char16 NewLine[] = { '\n', 0 }; char16 *e = Utf8ToWide(Email); if (e) { auto Len = StrlenW(Edit->NameW()); if (Len) Edit->Insert(Len, NewLine, 1); Edit->Insert(Len + 1, e, StrlenW(e)); } } } int GroupUi::OnDrop(LArray &Data, LPoint Pt, int KeyState) { int Status = DROPEFFECT_NONE; for (unsigned n=0; nIsBinary() && ScribeClipboardFmt::IsThing(Data->Value.Binary.Data, Data->Value.Binary.Length)) { ScribeClipboardFmt *Fmt = (ScribeClipboardFmt*)Data->Value.Binary.Data; for (unsigned i=0; iLength(); i++) { Contact *c = Fmt->ThingAt(i) ? Fmt->ThingAt(i)->IsContact() : NULL; if (c) { auto Email = c->GetAddrAt(0); if (Email) { Add(Email); Status = DROPEFFECT_COPY; } else LgiTrace("%s:%i - No addr\n", _FL); } else LgiTrace("%s:%i - Not a contact\n", _FL); } } } } else if (dd.IsFileDrop()) { LgiTrace("%s:%i - Impl file drop here\n", _FL); } } return Status; } //////////////////////////////////////////////////////////////////////////////////////// #define EDIT_OPEN_CONTACT 100 #define EDIT_DELETE 101 #define EDIT_ADDR_BASE 1000 #define GEDIT 'GEDT' AddressMeta::AddressMeta(LAddressEdit *edit, int id) : Addr(edit->App) { Edit = edit; Id = id < 0 ? edit->CreateId() : id; Referenced = false; LAssert(!Edit->Map.Find(Id)); Edit->Map.Add(Id, this); } AddressMeta::~AddressMeta() { LAssert(Edit->Map.Find(Id)); Edit->Map.Delete(Id, true); } bool AddressMeta::IsUnique() { return Addr.Length() == 1; } bool AddressMeta::IsResolved() { bool Status = false; if (IsUnique()) { RecipientItem *r = Addr[0]; if (r && ValidStr(r->GetEmail())) { Contact *c = r->GetContact(); if (c) { auto Emails = c->GetEmails(); for (auto e: Emails) { if (e.Equals(Text)) { Status = true; break; } } } else { Status = _stricmp(Text, r->GetEmail()) == 0; } } } return Status; } void AddressMeta::Ins() { RecipientItem *r = Addr[0]; if (r) { const char *n = Addr.sAddr; if (!n) n = r->GetEmail(); LAutoWString Name(Utf8ToWide(n)); if (Name) { /* auto v = Style->View; Edit->AllowPour = false; v->Delete(Style->Start, Style->Len); Edit->AllowPour = true; Style->Len = StrlenW(Name); auto sl = Style->End(); // This may delete this object... so don't use anything local afterwards v->Insert(Style->Start, Name, Style->Len); // No local! v->Invalidate(); v->SetCaret(sl, false, false); */ } } } void AddressMeta::OpenContact() { if (Addr.Length() == 1) { RecipientItem *r = Addr[0]; if (r && r->GetContact()) { r->GetContact()->DoUI(); } } } LAddressEdit::LAddressEdit(LRect *p, const char *t) : LTextView3(-1, 0, 0, 100, 100, 0) { App = NULL; AllowPour = true; if (p) SetPos(*p); Sunken(true); } void LAddressEdit::OnPaint(LSurface *pDC) { LTextView3::OnPaint(pDC); #if 0// def _DEBUG pDC->ClipRgn(0); char s[256]; sprintf_s(s, sizeof(s), "%i styles", Style.Length()); LSysFont->Colour(Rgb24(0, 0, 255), 24); LSysFont->Transparent(true); LDisplayString ds(LSysFont, s); ds.Draw(pDC, 2, Y()-20); #endif } void LAddressEdit::PourStyle(size_t Start, ssize_t Length) { if (AllowPour) { const char *Delim = " \t\r\n,;"; LUnrolledList Old, New; Old.Swap(Style); for (auto i : Map) { i.value->Referenced = false; } LHashTbl,LStyle*> Hash; for (auto &i : Old) { LAssert(i.Data.Type != GV_NULL); Hash.Add(i.Start, &i); } // Generate the new list... for (int i=0; i 0) { // Insert new style LAssert(App != NULL); auto &s = New.New().Construct(this, STYLE_ADDRESS); s.Start = Start; s.Len = Len; } } // Match the old and new lists, merging unchanged entries from // the old list and taking changed entries from the new list. AddressMeta *m; for (auto &n : New) { // Find matching old entry LString s(Text + n.Start, n.Len); auto o = Hash.Find(n.Start); if (o && o->Len == n.Len) { // Carry over the data ref id LAssert(o->Data.Type != GV_NULL); n.Data = o->Data; n.Font = o->Font; n.Fore = o->Fore; n.Back = o->Back; n.Decor = o->Decor; n.DecorColour = o->DecorColour; auto m = GetMeta(n); if (m) { // LgiTrace("Existing meta @ %i '%s'\n", n.Start, m->Text.Get()); m->Referenced = true; } } else { // Create a new entry if ((m = new AddressMeta(this))) { n.Data = m->Id; m->Referenced = true; m->Text.SetW(Text + n.Start, n.Len); UpdateMeta(n); // LgiTrace("Creating meta @ %i '%s' with id=%i\n", n.Start, m->Text.Get(), m->Id); } } } // Clear out unreferenced meta objects for (auto i : Map) { if (i.value->Referenced == false) { // LgiTrace("Deleting unreferenced meta '%s'\n", i.value->Text.Get()); delete i.value; } } #ifdef _DEBUG for (auto &s : New) { LAssert(s.Data.Type != GV_NULL); } #endif Style.Swap(New); // Update LRect r(0, Y()-20, 100, Y()); Invalidate(&r); #if 0 printf("Styles:\n"); for (GEditAddress *e=(GEditAddress*)Style.First(); e; e=(GEditAddress*)Style.Next()) { printf("\tStart=%i Len=%i Text=%.*S\n", e->Start, e->Len, e->Len, Text+e->Start); } #endif } } bool LAddressEdit::Insert(size_t At, const char16 *Data, ssize_t Len) { for (auto &s : Style) { LAssert(s.Data.Type != GV_NULL); if (s.Start > (ssize_t)At) { /*auto m =*/ GetMeta(s); // LgiTrace("Insert adding to start: %i->%i '%s'\n", s.Start, s.Start + Len, m ? m->Text.Get() : NULL); s.Start += Len; } } return LTextView3::Insert(At, Data, Len); } bool LAddressEdit::Delete(size_t at, ssize_t Len) { ssize_t At = (ssize_t)at; for (auto &s : Style) { LAssert(s.Data.Type != GV_NULL); AddressMeta *m = GetMeta(s); if (s.Overlap(At, Len) && m && m->Matched()) { // extend delete region to include the whole styled addr if (At > s.Start) { Len += At - s.Start; At = s.Start; } if (At + Len < s.Start + s.Len) { Len = s.Start + s.Len - At; } // LgiTrace("Delete len change: %i:%i '%s'\n", s.Start, s.Len, m ? m->Text.Get() : NULL); } if (s.Start > At) { s.Start -= Len; // LgiTrace("Delete start change: %i:%i '%s'\n", s.Start, s.Len, m ? m->Text.Get() : NULL); } } return LTextView3::Delete(At, Len); } bool LAddressEdit::GetStyles(List &Styles) { /* Styles.Empty(); for (GEditAddress *s = (GEditAddress*) Style.First(); s; s = (GEditAddress*) Style.Next()) { Styles.Insert(s); } return Styles.First() != 0; */ return false; } int LAddressEdit::WillAccept(LDragFormats &Formats, LPoint Pt, int KeyState) { LWindow *w = GetWindow(); LDragDropTarget *t = dynamic_cast(w); if (t) { return t->WillAccept(Formats, Pt, KeyState); } return DROPEFFECT_NONE; } bool LAddressEdit::OnStyleClick(LStyle *style, LMouse *ms) { auto m = GetMeta(*style); if (!m) return false; if (ms->Double() && ms->Left()) { m->OpenContact(); return true; } return false; } bool LAddressEdit::OnStyleMenu(LStyle *style, LSubMenu *sub) { auto m = GetMeta(*style); if (!m) return false; if (m->Addr.Length() == 1) { sub->AppendItem("Open Contact", EDIT_OPEN_CONTACT, true); sub->AppendItem("Delete", EDIT_DELETE, true); } else { Items.DeleteObjects(); AddressBrowseLookup(App, Items, m->Text); if (Items.Length()) { int Idx = 0; for (auto i : Items) { LString n; n.Printf("%s %s <%s>", i->First.Get(), i->Last.Get(), i->Email.Get()); sub->AppendItem(n, EDIT_ADDR_BASE + Idx++); } } else { sub->AppendItem("No results", -1, false); } } sub->AppendSeparator(); /* if (IsOk() && Addr.Length() > 0) { if (Addr.Length() == 1) { m->AppendItem("Open Contact", EDIT_OPEN_CONTACT, true); m->AppendItem("Delete", EDIT_DELETE, true); m->AppendSeparator(); } char s[256]; for (int i=0; iGetContact(); if (c) { LAutoString Email; for (int n=0; (Email = c->GetAddrAt(n)); n++) { char *First = 0, *Last = 0; s[0] = 0; c->Get(OPT_First, First); c->Get(OPT_Last, Last); if (First) { if (*s) strcat(s, " "); strcat(s, First); } if (Last) { if (*s) strcat(s, " "); strcat(s, Last); } int len = (int)strlen(s); sprintf_s(s+len, sizeof(s)-len, " <%s>", (char*)Email); int k = (i << 8) + n; m->AppendItem( s, EDIT_ADDR_BASE + k, ValidStr(Email)); LgiTrace("Adding contact '%s' %i:%i (%k)\n", s, i, n, k); } } else { char *Name = Addr[i]->GetName(); char *Email = Addr[i]->GetEmail(); sprintf_s(s, sizeof(s), "%s <%s>", Name, Email); m->AppendItem( s, EDIT_ADDR_BASE + (i << 8), ValidStr(Email)); } } return true; } */ return false; } void LAddressEdit::OnStyleMenuClick(LStyle *style, int i) { auto m = GetMeta(*style); if (!m) return; switch (i) { case EDIT_OPEN_CONTACT: { m->OpenContact(); break; } case EDIT_DELETE: { // This will delete us, don't access anything local afterwards Delete(style->Start, style->Len); // Update the edit and get outta here Invalidate(); return; break; } default: { BrowseItem *r = Items[i - EDIT_ADDR_BASE]; if (r) { Contact *c = Contact::LookupEmail(r->Email); if (c) { style->Fore = LColour(L_TEXT); m->Addr.SetWho(new RecipientItem(c), -1); LAutoWString wEmail(Utf8ToWide(r->Email)); auto Start = style->Start; auto Len = style->Len; Delete(Start, Len); Insert(Start, wEmail, Strlen(wEmail.Get())); } } } } } int LAddressEdit::OnDrop(LArray &Data, LPoint Pt, int KeyState) { LWindow *w = GetWindow(); LDragDropTarget *t = dynamic_cast(w); if (t) { return t->OnDrop(Data, Pt, KeyState); } return DROPEFFECT_NONE; } class LAddressEditFactory : public LViewFactory { public: LView *NewView(const char *Class, LRect *Pos, const char *Text) { if (strcmp(Class, "LAddressEdit") == 0) { return new LAddressEdit(Pos, Text); } return 0; } } AddressEditFactory; ////////////////////////////////////////////////////////////////////////////// LGroupMap::LGroupMap(ScribeWnd *app) : App(app) { auto srcs = App->GetThingSources(MAGIC_GROUP); for (auto s: srcs) { s->LoadThings(); for (auto i: s->Items) Index(i->IsGroup()); } } LGroupMap::~LGroupMap() { DeleteObjects(); } void LGroupMap::Index(ContactGroup *grp) { if (!grp) return; for (auto email: grp->GetAddresses()) { auto a = Find(email); if (!a) { a = new LGroupMapArray; Add(email, a); } a->Add(grp); } } diff --git a/Code/ScribeMail.cpp b/Code/ScribeMail.cpp --- a/Code/ScribeMail.cpp +++ b/Code/ScribeMail.cpp @@ -1,10254 +1,10254 @@ /* ** FILE: ScribeMail.cpp ** AUTHOR: Matthew Allen ** DATE: 11/11/98 ** DESCRIPTION: Scribe Mail Object and UI ** ** Copyright (C) 1998-2003, Matthew Allen ** fret@memecode.com */ #include #include #include #include #include #include #include "Scribe.h" #include "ScribePageSetup.h" #include "lgi/common/NetTools.h" #include "lgi/common/Popup.h" #include "lgi/common/ColourSelect.h" #include "lgi/common/TextView3.h" #include "lgi/common/Html.h" #include "lgi/common/Combo.h" #include "lgi/common/Edit.h" #include "lgi/common/Button.h" #include "lgi/common/TextLabel.h" #include "lgi/common/CheckBox.h" #include "lgi/common/TabView.h" #include "lgi/common/Input.h" #include "lgi/common/ClipBoard.h" #include "lgi/common/TableLayout.h" #include "lgi/common/DisplayString.h" #include "lgi/common/ThreadEvent.h" #include "lgi/common/GdcTools.h" #include "lgi/common/Charset.h" #include "../src/common/Coding/ScriptingPriv.h" #include "PrintPreview.h" #include "ScribeListAddr.h" #include "PrintContext.h" #include "lgi/common/LgiRes.h" #include "Encryption/GnuPG.h" #include "ObjectInspector.h" #include "lgi/common/EventTargetThread.h" #include "Store3Common.h" #include "Tables.h" #include "Calendar.h" #include "CalendarView.h" #include "AddressSelect.h" #include "Store3Imap/ScribeImap.h" #include "lgi/common/TextConvert.h" #include "lgi/common/FileSelect.h" #include "resdefs.h" #include "resource.h" #include "lgi/common/Printer.h" #include "lgi/common/SubProcess.h" #define SAVE_HEADERS 0 static char MsgIdEncodeChars[] = "%/"; static char ScribeReplyClass[] = "scribe_reply"; static char ScribeReplyStyles[] = "margin-left: 0.5em;\n" "padding-left: 0.5em;\n" "border-left: 1px solid #ccc;"; char DefaultTextReplyTemplate[] = { "---------- Original Message ----------\n" "To: <>\n" "From: <>\n" "Subject: \n" "Date: \n" "\n" "\n" "\n" "\n" }; char DefaultHtmlReplyTemplate[] = { "\n" "\n" "\n" "\n" "\n" "

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

\n" "\n" "\n" }; class DepthCheck { int &i; public: constexpr static int MaxDepth = 5; DepthCheck(int &val) : i(val) { i++; if (!*this) LAssert(!"Recursion?"); } ~DepthCheck() { i--; } operator bool() const { return i < MaxDepth; } }; void CollectAttachments(LArray *Attachments, LArray *Related, LDataI **Text, LDataI **Html, LDataPropI *d, Store3MimeType *ParentMime = NULL) { if (!d) return; Store3MimeType Mt(d->GetStr(FIELD_MIME_TYPE)); auto FileName = d->GetStr(FIELD_NAME); if (!Mt) Mt = "text/plain"; // printf("Collect %s %s\n", (char*)Mt, FileName); auto Att = dynamic_cast(d); if (ParentMime && Att && ParentMime->IsRelated() && Related) { auto Id = d->GetStr(FIELD_CONTENT_ID); if (ValidStr(Id)) { if (Related) Related->Add(Att); Att = NULL; } else if (Mt.IsHtml() && Html) { *Html = Att; Att = NULL; } } if (Att) { if (ValidStr(FileName)) { if (Attachments) Attachments->Add(Att); } else if (Mt.IsHtml()) { if (Html) *Html = Att; } else if (Mt.IsPlainText()) { if (Text) *Text = Att; } else if (!Mt.IsMultipart()) { if (Attachments) Attachments->Add(Att); } /* if (d->GetInt(FIELD_SIZE) < (512 << 10)) a.Add(dynamic_cast(d)); */ } auto It = d->GetList(FIELD_MIME_SEG); if (It) { for (auto i = It->First(); i; i = It->Next()) CollectAttachments(Attachments, Related, Text, Html, i, Mt.IsMultipart() ? &Mt : NULL); } } void RemoveReturns(char *s) { // Delete out the '\r' chars. char *In = s; char *Out = s; while (*In) { if (*In != '\r') { *Out++ = *In; } In++; } *Out++ = 0; } class XmlSaveStyles : public LXmlTree { void OnParseComment(LXmlTag *Ref, const char *Comment, ssize_t Bytes) { if (Ref && Ref->IsTag("style")) { Ref->SetContent(Comment, Bytes); } } public: XmlSaveStyles(int flags) : LXmlTree(flags) { } }; bool ExtractHtmlContent(LString &OutHtml, LString &Charset, LString &Styles, const char *InHtml) { if (!InHtml) return false; XmlSaveStyles t(GXT_NO_ENTITIES | GXT_NO_DOM | GXT_NO_HEADER); LXmlTag r; LMemStream mem(InHtml, strlen(InHtml), false); if (!t.Read(&r, &mem)) return false; bool InHead = false; LStringPipe Style; r.Children.SetFixedLength(false); for (auto It = r.Children.begin(); It != r.Children.end(); ) { LXmlTag *c = *It; if (c->IsTag("style")) { if (ValidStr(c->GetContent())) Style.Print("%s\n", c->GetContent()); c->Parent = NULL; r.Children.Delete(It); DeleteObj(c); } else if (c->IsTag("/head")) { InHead = false; c->Parent = NULL; r.Children.Delete(It); DeleteObj(c); } else if (c->IsTag("body")) { // We remove this tag, but KEEP the content... if any if (ValidStr(c->GetContent())) { c->SetTag(NULL); It++; } else { // No content, remove entirely. c->Parent = NULL; r.Children.Delete(It); DeleteObj(c); } } else if (InHead || c->IsTag("html") || c->IsTag("/html") || c->IsTag("/body") || c->IsTag("/style")) { c->Parent = NULL; r.Children.Delete(It); DeleteObj(c); } else if (c->IsTag("head")) { InHead = true; c->Parent = NULL; r.Children.Delete(It); DeleteObj(c); } else It++; } LStringPipe p; t.Write(&r, &p); OutHtml = p.NewLStr(); Styles = Style.NewLStr(); #if 0 LgiTrace("InHtml=%s\n", InHtml); LgiTrace("OutHtml=%s\n", OutHtml.Get()); LgiTrace("Styles=%s\n", Styles.Get()); #endif return true; } ////////////////////////////////////////////////////////////////////////////// char MailToStr[] = "mailto:"; char SubjectStr[] = "subject="; char ContentTypeDefault[] = "Content-type: text/plain; charset=us-ascii"; extern LString HtmlToText(const char *Html, const char *InitialCharSet); extern LString TextToHtml(const char *Txt, const char *Charset); class ImageResizeThread : public LEventTargetThread { LOptionsFile *Opts; public: class Job { #ifdef __GTK_H__ /* This object may not exist when the worker is finished. However LAppInst->PostEvent can handle that so we'll allow it, so long as it's never used in the Sink->PostEvent form. */ LViewI *Sink; #else OsView Sink; #endif public: LString FileName; LAutoStreamI Data; void SetSink(LViewI *v) { #if !LGI_VIEW_HANDLE Sink = v; #else Sink = v->Handle(); #endif } bool PostEvent(int Msg, LMessage::Param a = 0, LMessage::Param b = 0) { #ifdef __GTK_H__ return LAppInst->PostEvent(Sink, Msg, a, b); #else return LPostEvent(Sink, Msg, a, b); #endif } }; ImageResizeThread(LOptionsFile *opts) : LEventTargetThread("ImageResize") { Opts = opts; } void Resize(LAutoPtr &Job) { LVariant Qual = 80, Px = 1024, SizeLimit = 200, v; Opts->GetValue(OPT_ResizeJpegQual, Qual); Opts->GetValue(OPT_ResizeMaxPx, Px); Opts->GetValue(OPT_ResizeMaxKb, SizeLimit); LAutoStreamI Input = Job->Data; LAutoStreamI MemBuf(new LMemFile(4 << 10)); int64 FileSize = Input->GetSize(); LStream *sImg = dynamic_cast(Input.Get()); LAutoPtr Img(GdcD->Load(sImg, Job->FileName)); if (Img) { int iPx = Px.CastInt32(); int iKb = SizeLimit.CastInt32(); if (Img->X() > iPx || Img->Y() > iPx || FileSize >= iKb << 10) { // Create a JPEG filter auto Jpeg = LFilterFactory::New(".jpg", FILTER_CAP_WRITE, NULL); if (Jpeg) { // Re-sample the image... double XScale = (double) Img->X() / iPx; double YScale = (double) Img->Y() / iPx; // double Aspect = (double) Img->X() / Img->Y(); double Scale = XScale > YScale ? XScale : YScale; if (Scale > 1.0) { int Nx = (int)(Img->X() / Scale + 0.001); int Ny = (int)(Img->Y() / Scale + 0.001); LAutoPtr ResizedImg(new LMemDC(Nx, Ny, Img->GetColourSpace())); if (ResizedImg) { if (ResampleDC(ResizedImg, Img)) { Img = ResizedImg; } } } // Compress the image.. LXmlTag Props; Props.SetValue(LGI_FILTER_QUALITY, Qual); Props.SetValue(LGI_FILTER_SUBSAMPLE, v = 1); // 2x2 Jpeg->Props = &Props; if (Jpeg->WriteImage(dynamic_cast(MemBuf.Get()), Img) == LFilter::IoSuccess) { Job->Data = MemBuf; } } } } Job->PostEvent(M_RESIZE_IMAGE, (LMessage::Param)Job.Get()); Job.Release(); // Do this after the post event... so the deref doesn't crash. } LMessage::Result OnEvent(LMessage *Msg) { switch (Msg->Msg()) { case M_RESIZE_IMAGE: { LAutoPtr j((Job*)Msg->A()); if (j) Resize(j); break; } } return 0; } }; #include "lgi/common/RichTextEdit.h" #include "../src/common/Widgets/Editor/RichTextEditPriv.h" class MailRendererScript : public LThread, public LCancel, public LDom { Mail *m; LScriptCallback *cb; public: MailRendererScript(Mail *ml, LScriptCallback *script) : m(ml), cb(script), LThread("MailRendererScript") { Run(); } ~MailRendererScript() { Cancel(true); while (!IsExited()) LSleep(1); } int Main() { LVirtualMachine Vm; LScriptArguments Args(&Vm); Args.New() = new LVariant(m->App); Args.New() = new LVariant(m); Args.New() = new LVariant((LDom*)this); m->App->ExecuteScriptCallback(*cb, Args); Args.DeleteObjects(); return 0; } bool CallMethod(const char *MethodName, LVariant *Ret, LArray &Args) { ScribeDomType Method = StrToDom(MethodName); *Ret = false; switch (Method) { case SdGetTemp: { *Ret = ScribeTempPath(); break; } case SdExecute: { *Ret = false; if (Args.Length() >= 3) { auto Dir = Args[0]->Str(); auto Exe = Args[1]->Str(); auto Arg = Args[2]->Str(); if (Dir && Exe && Arg) { LSubProcess p(Exe, Arg); p.SetInitFolder(Dir); if (p.Start()) { char Buf[256]; LStringPipe Out; Out.Print("%s %s\n", Exe, Arg); ssize_t Rd; while (p.IsRunning() && !IsCancelled()) { Rd = p.Read(Buf, sizeof(Buf)); if (Rd < 0) break; if (Rd == 0) LSleep(1); else Out.Write(Buf, Rd); } while (!IsCancelled() && (Rd = p.Read(Buf, sizeof(Buf))) > 0) Out.Write(Buf, Rd); // LgiTrace("%s:%i - Process:\n%s\n", _FL, Out.NewLStr().Get()); if (p.IsRunning()) p.Kill(); else *Ret = true; } } } break; } case SdSetHtml: { if (!IsCancelled() && Args.Length() > 0) { LView *Ui = m->GetUI(); if (!Ui) { // Maybe hasn't finished opening the UI? LSleep(100); Ui = m->GetUI(); } if (!Ui) Ui = m->App; if (Ui) Ui->PostEvent(M_SET_HTML, (LMessage::Param)new LString(Args[0]->Str())); } break; } default: LAssert(!"Unsupported call."); return false; } return true; } }; class MailPrivate { LAutoPtr Resizer; Mail *m; public: struct HtmlBody { LString Html, Charset, Styles; }; LAutoPtr Body; LAutoPtr Renderer; LString MsgIdCache; LString DomainCache; int InSetFlags = 0; int InSetFlagsCache = 0; MailPrivate(Mail *mail) { m = mail; } void OnSave() { Resizer.Reset(); } HtmlBody *GetBody() { if (!Body && !Body.Reset(new HtmlBody)) return NULL; if (ValidStr(m->GetHtml())) { ExtractHtmlContent( Body->Html, Body->Charset, Body->Styles, m->GetHtml()); } else if (ValidStr(m->GetBody())) { Body->Charset = m->GetCharSet(); Body->Html = TextToHtml(m->GetBody(), Body->Charset); } return Body; } bool AddResizeImage(Attachment *File) { if (!File || !m->GetUI()) return false; if (!Resizer) Resizer.Reset(new ImageResizeThread(m->App->GetOptions())); if (!Resizer) return false; LAutoStreamI Obj = File->GetObject()->GetStream(_FL); if (!Obj) return false; ImageResizeThread::Job *j = new ImageResizeThread::Job; if (!j) return false; // The user interface will handle the response... j->SetSink(m->GetUI()); j->FileName = File->GetName(); // Make a complete copy of the stream... j->Data.Reset(new LMemStream(Obj, 0, -1)); // Post the work over to the thread... Resizer->PostEvent(M_RESIZE_IMAGE, (LMessage::Param)j); // Mark the image resizing File->SetIsResizing(true); return true; } }; bool Mail::ResizeImage(Attachment *a) { return d->AddResizeImage(a); } AttachmentList::AttachmentList(int id, int x, int y, int cx, int cy, MailUi *ui) : LList(id, x, y, cx, cy, 0) { Ui = ui; } AttachmentList::~AttachmentList() { RemoveAll(); } void AttachmentList::OnItemClick(LListItem *Item, LMouse &m) { LList::OnItemClick(Item, m); if (!Item && m.IsContextMenu() && Ui) { LSubMenu RClick; RClick.AppendItem(LLoadString(IDS_ATTACH_FILE), IDM_OPEN, true); switch (RClick.Float(this, m)) { case IDM_OPEN: { Ui->PostEvent(M_COMMAND, IDM_ATTACH_FILE, 0); break; } } } } bool AttachmentList::OnKey(LKey &k) { if (k.vkey == LK_DELETE) { if (k.Down()) { List s; if (GetSelection(s)) { Attachment *a = dynamic_cast(s[0]); if (a) { a->OnDeleteAttachment(this, true); } } } return true; } return LList::OnKey(k); } ////////////////////////////////////////////////////////////////////////////// int Strnlen(const char *s, int n) { int i = 0; if (s) { if (n < 0) { while (*s++) { i++; } } else { while (*s++ && n-- > 0) { i++; } } } return i; } ////////////////////////////////////////////////////////////////////////////// MailContainer::~MailContainer() { MailContainerIter *i; while ((i = Iters[0])) { Iters.Delete(i); if (i->Container) { i->Container = 0; } } } MailContainerIter::MailContainerIter() { Container = 0; } MailContainerIter::~MailContainerIter() { if (Container) { Container->Iters.Delete(this); } } void MailContainerIter::SetContainer(MailContainer *c) { Container = c; if (Container) { Container->Iters.Insert(this); } } ////////////////////////////////////////////////////////////////////////////// uint32_t MarkColours32[IDM_MARK_MAX] = { Rgb32(255, 0, 0), // red Rgb32(255, 166, 0), // orange Rgb32(255, 222, 0), // yellow Rgb32(0, 0xa0, 0), // green Rgb32(0, 0xc0, 255),// cyan Rgb32(0, 0, 255), // blue Rgb32(192, 0, 255), // purple Rgb32(0, 0, 0) // black }; ItemFieldDef MailFieldDefs[] = { {"To", SdTo, GV_STRING, FIELD_TO}, {"From", SdFrom, GV_STRING, FIELD_FROM}, {"Subject", SdSubject, GV_STRING, FIELD_SUBJECT}, {"Size", SdSize, GV_INT64, FIELD_SIZE}, {"Received Date", SdReceivedDate, GV_DATETIME, FIELD_DATE_RECEIVED}, {"Send Date", SdSendDate, GV_DATETIME, FIELD_DATE_SENT}, {"Body", SdBody, GV_STRING, FIELD_TEXT}, {"Internet Header", SdInternetHeader, GV_STRING, FIELD_INTERNET_HEADER, IDC_INTERNET_HEADER}, {"Message ID", SdMessageID, GV_STRING, FIELD_MESSAGE_ID}, {"Priority", SdPriority, GV_INT32, FIELD_PRIORITY}, {"Flags", SdFlags, GV_INT32, FIELD_FLAGS}, {"Html", SdHtml, GV_STRING, FIELD_ALTERNATE_HTML}, {"Label", SdLabel, GV_STRING, FIELD_LABEL}, {"From Contact", SdContact, GV_STRING, FIELD_FROM_CONTACT_NAME}, {"File", SdFile, GV_STRING, FIELD_CACHE_FILENAME}, {"ImapFlags", SdImapFlags, GV_STRING, FIELD_CACHE_FLAGS}, {"ImapSeq", SdFile, GV_INT32, FIELD_IMAP_SEQ}, {"ImapUid", SdImapFlags, GV_INT32, FIELD_SERVER_UID}, {"ReceivedDomain", SdReceivedDomain, GV_STRING, FIELD_RECEIVED_DOMAIN}, {"MessageId", SdMessageId, GV_STRING, FIELD_MESSAGE_ID}, {0} }; ////////////////////////////////////////////////////////////////////////////// class LIdentityItem : public LListItem { ScribeWnd *App; ScribeAccount *Acc; char *Txt; public: LIdentityItem(ScribeWnd *app, ScribeAccount *acc) { App = app; Acc = acc; Txt = 0; LVariant e, n; if (Acc) { n = Acc->Identity.Name(); e = Acc->Identity.Email(); } else { LAssert(!"No account specified"); } if (e.Str() && n.Str()) { char t[256]; sprintf_s(t, sizeof(t), "%s <%s>", n.Str(), e.Str()); Txt = NewStr(t); } else if (e.Str()) { Txt = NewStr(e.Str()); } else if (n.Str()) { Txt = NewStr(n.Str()); } else { Txt = NewStr("(error)"); } } ~LIdentityItem() { DeleteArray(Txt); } ScribeAccount *GetAccount() { return Acc; } const char *GetText(int i) { switch (i) { case 0: { return Txt; break; } } return 0; } }; class LIdentityDropDrop : public LPopup { ScribeWnd *App; Mail *Email; LList *Lst; public: LIdentityDropDrop(ScribeWnd *app, Mail *mail, LView *owner) : LPopup(owner) { App = app; Email = mail; LRect r(0, 0, 300, 100); SetPos(r); Children.Insert(Lst = new LList(IDC_LIST, 2, 2, X()-4, Y()-4)); if (Lst) { Lst->SetParent(this); Lst->AddColumn("Identity", Lst->GetClient().X()); Lst->MultiSelect(false); if (App) { Lst->Insert(new LIdentityItem(App, 0)); for (auto a : *App->GetAccounts()) { if (a->Identity.Name().Str()) { Lst->Insert(new LIdentityItem(App, a)); } } /* for (LListItem *i = List->First(); i; i = List->Next()) { LIdentityItem *Item = dynamic_cast(i); if (Item) { char *IdEmail = a->Send.IdentityEmail(); if (Email && IdEmail && Email->From->Addr && _stricmp(Email->From->Addr, IdEmail) == 0) { Item->Select(true); } } } */ } } } void OnPaint(LSurface *pDC) { LRect r(GetClient()); LWideBorder(pDC, r, DefaultRaisedEdge); } int OnNotify(LViewI *c, LNotification n) { switch (c->GetId()) { case IDC_LIST: { if (n.Type == LNotifyItemClick) { Visible(false); LIdentityItem *NewFrom = dynamic_cast(Lst->GetSelected()); if (Email && NewFrom) { // ScribeAccount *a = NewFrom->GetAccount(); if (Email->GetUI()) { LList *FromList; if (GetViewById(IDC_FROM, FromList)) { // Change item data /* FIXME DeleteArray(Email->From->Name); DeleteArray(Email->From->Addr); DeleteArray(Email->Reply->Name); DeleteArray(Email->Reply->Addr); if (a) { Email->From->Addr = NewStr(a->Send.IdentityEmail().Str()); Email->From->Name = NewStr(a->Send.IdentityName().Str()); Email->Reply->Addr = NewStr(a->Send.IdentityReplyTo().Str()); if (Email->Reply->Addr) { Email->Reply->Name = NewStr(Email->From->Name); } } else { LVariant e, n, r; App->GetOptions()->GetValue(OPT_EmailAddr, e); App->GetOptions()->GetValue(OPT_UserName, n); App->GetOptions()->GetValue(OPT_ReplyToEmail, n); Email->From->Addr = NewStr(e.Str()); Email->From->Name = NewStr(n.Str()); Email->Reply->Addr = NewStr(r.Str()); if (Email->Reply->Addr) { Email->Reply->Name = NewStr(Email->From->Name); } } // Change UI FromList->Empty(); LDataPropI *na = new LDataPropI(Email->From); if (na) { na->CC = MAIL_ADDR_FROM; FromList->Insert(na); } */ } } } } break; } } return 0; } }; ////////////////////////////////////////////////////////////////////////////// LSubMenu* BuildMarkMenu( LSubMenu *MarkMenu, MarkedState MarkState, uint32_t SelectedMark, bool None, bool All, bool Select) { int SelectedIndex = -1; // Build image list LImageList *ImgLst = new LImageList(16, 16); if (ImgLst && ImgLst->Create(16 * CountOf(MarkColours32), 16, System32BitColourSpace)) { // ImgLst->Colour(1); ImgLst->Colour(L_MED); ImgLst->Rectangle(); for (int i=0; iColour(L_LOW); ImgLst->Box(i*16+1, 0, i*16+15, 14); SelectedIndex = i; } ImgLst->Colour(MarkColours32[i], 32); ImgLst->Rectangle(i*16+3, 2, i*16+13, 12); } } // Build Submenu if (MarkMenu && ImgLst) { ImgLst->Update(-1); MarkMenu->SetImageList(ImgLst); LMenuItem *Item = NULL; if (None) { Item = MarkMenu->AppendItem(LLoadString(IDS_NONE), Select ? IDM_SELECT_NONE : IDM_UNMARK, MarkState != MS_None); } if (All) { Item = MarkMenu->AppendItem(LLoadString(IDS_ALL), Select ? IDM_SELECT_ALL : IDM_MARK_ALL, true); } if (Item) { MarkMenu->AppendSeparator(); } for (int i=0; iAppendItem(s, ((Select) ? IDM_MARK_SELECT_BASE : IDM_MARK_BASE) + i, (MarkState != 1) || (i != SelectedIndex)); if (Item) { Item->Icon(i); } } } return MarkMenu; } ////////////////////////////////////////////////////////////////////////////// char *WrapLines(char *Str, int Len, int WrapColumn) { if (Str && Len > 0 && WrapColumn > 0) { LMemQueue Temp; int LastWhite = -1; int StartLine = 0; int XPos = 0; int i; for (i=0; Str[i] && i= WrapColumn && Len > 0) { Temp.Write((uchar*) Str+StartLine, Len); Temp.Write((uchar*) "\n", 1); XPos = 0; StartLine = StartLine + Len + 1; LastWhite = -1; } else { LastWhite = i; XPos++; } } else if (Str[i] == '\t') { XPos = ((XPos + 7) / 8) * 8; } else if (Str[i] == '\n') { Temp.Write((uchar*) Str+StartLine, i - StartLine + 1); XPos = 0; StartLine = i+1; } else { XPos++; } } Temp.Write((uchar*) Str+StartLine, i - StartLine + 1); int WrapLen = (int)Temp.GetSize(); char *Wrapped = new char[WrapLen+1]; if (Wrapped) { Temp.Read((uchar*) Wrapped, WrapLen); Wrapped[WrapLen] = 0; return Wrapped; } } return Str; } char *DeHtml(const char *Str) { char *r = 0; if (Str) { LMemQueue Buf; char Buffer[256]; auto s = Str; while (s && *s) { // Search for start of next tag const char *Start = s; const char *End = Start; for (; *End && *End != '<'; End++); // Push pre-tag data onto pipe size_t Len = End-Start; for (size_t i=0; i, ItemFieldDef*> Lut(256); if (Lut.Length() == 0) { for (ItemFieldDef **l=FieldLists; *l; l++) { for (ItemFieldDef *i = *l; i->FieldId; i++) { if (i->Option) Lut.Add(i->Option, i); } } } return (ItemFieldDef*) Lut.Find(Name); } return 0; } static bool FieldLutInit = false; static LArray IdToFieldLut; ItemFieldDef *GetFieldDefById(int Id) { if (!FieldLutInit) { FieldLutInit = true; for (ItemFieldDef **l=FieldLists; *l; l++) { for (ItemFieldDef *i = *l; i->FieldId; i++) { IdToFieldLut[i->FieldId] = i; } } } if (Id >= 0 && Id < (int)IdToFieldLut.Length()) return IdToFieldLut[Id]; return 0; } ////////////////////////////////////////////////////////////////////////////// void Log(char *File, char *Str, ...) { #if defined WIN32 const char *DefFile = "c:\\temp\\list.txt"; #else const char *DefFile = "/home/list.txt"; #endif if (Str) { LFile f; if (f.Open((File) ? File : DefFile, O_WRITE)) { char Buf[1024]; va_list Arg; va_start(Arg, Str); vsprintf_s(Buf, sizeof(Buf), Str, Arg); va_end(Arg); f.Seek(0, SEEK_END); f.Write(Buf, (int)strlen(Buf)); } } else { LFile f; if (f.Open((File) ? File : DefFile, O_WRITE)) { f.SetSize(0); } } } char *NewPropStr(LOptionsFile *Options, char *Name) { LVariant n; if (Options->GetValue(Name, n) && n.Str() && strlen(n.Str()) > 0) { return NewStr(n.Str()); } return 0; } ////////////////////////////////////////////////////////////////////////////// // Columns of controls #define MAILUI_Y 0 #define IDM_REMOVE_GRTH 1000 #define IDM_REMOVE_GRTH_SP 1001 #define IDM_REMOVE_HTML 1002 #define IDM_CONVERT_B64_TO_BIN 1003 #define IDM_CONVERT_BIN_TO_B64 1004 #define RECIP_SX 500 #define ADD_X (RECIP_X + RECIP_SX + 10) #ifdef MAC #define ADD_RECIP_BTN_X 36 #else #define ADD_RECIP_BTN_X 20 #endif #define CONTENT_BORDER 3 #if defined WIN32 #define DLG_X 15 #define DLG_Y 30 #else #define DLG_X 6 #define DLG_Y 6 #endif MailUi::MailUi(Mail *item, MailContainer *container) : ThingUi(item, LLoadString(IDS_MAIL_MESSAGE)), WorkingDlg(NULL), Sx(0), Sy(0), CmdAfterResize(NULL), MissingCaps(NULL), BtnPrev(NULL), BtnNext(NULL), BtnSend(NULL), BtnSave(NULL), BtnSaveClose(NULL), BtnAttach(NULL), BtnReply(NULL), BtnReplyAll(NULL), BtnForward(NULL), BtnBounce(NULL), GpgUi(NULL), ToPanel(NULL), Entry(NULL), Browse(NULL), SetTo(NULL), To(NULL), Remove(NULL), FromPanel(NULL), FromList(NULL), FromCbo(NULL), ReplyToPanel(NULL), ReplyToChk(NULL), ReplyToCbo(NULL), SubjectPanel(NULL), Subject(NULL), CalendarPanel(NULL), CalPanelStatus(NULL), Tab(NULL), TabText(NULL), TextView(NULL), TabHtml(NULL), HtmlView(NULL), TabAttachments(NULL), Attachments(NULL), TabHeader(NULL), Header(NULL) { // Init everything to 0 Container = container; if (!item || !item->App) { LAssert(!"Invalid ptrs"); return; } AddMode = MAIL_ADDR_TO; CurrentEditCtrl = -1; MetaFieldsDirty = IgnoreShowImgNotify = HtmlCtrlDirty = TextCtrlDirty = TextLoaded = HtmlLoaded = false; // This allows us to hook iconv conversion events LFontSystem::Inst()->Register(this); // This allows us to hook missing image library events GdcD->Register(this); // Read/Write access bool ReadOnly = !TestFlag(GetItem()->GetFlags(), MAIL_CREATED | MAIL_BOUNCE); int MinButY = 0; // Get position LRect r(150, 150, 800, 750); SetPos(r); int FontHeight = GetFont()->GetHeight(); LVariant v; LOptionsFile *Options = App ? App->GetOptions() : 0; if (Options) { if (Options->GetValue("MailUI.Pos", v)) { r.SetStr(v.Str()); } } SetPos(r); MoveSameScreen(App); Name(LLoadString(IDS_MAIL_MESSAGE)); #if WINNATIVE CreateClassW32("Scribe::MailUi", LoadIcon(LProcessInst(), MAKEINTRESOURCE(IDI_MAIL))); #endif bool IsCreated = TestFlag(GetItem()->GetFlags(), MAIL_CREATED); if (Attach(0)) { DropTarget(true); // Setup main toolbar Commands.Toolbar = App->LoadToolbar(this, App->GetResourceFile(ResToolbarFile), App->GetToolbarImgList()); if (Commands.Toolbar) { Commands.Toolbar->Raised(false); Commands.Toolbar->Attach(this); BtnSend = Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_SEND)), IDM_SEND_MSG, TBT_PUSH, !ReadOnly, IMG_SEND); Commands.Toolbar->AppendSeparator(); BtnSave = Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_SAVE)), IDM_SAVE, TBT_PUSH, !ReadOnly, IMG_SAVE); BtnSaveClose = Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_SAVE_CLOSE)), IDM_SAVE_CLOSE, TBT_PUSH, !ReadOnly, IMG_SAVE_AND_CLOSE); Commands.Toolbar->AppendSeparator(); Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_DELETE)), IDM_DELETE_MSG, TBT_PUSH, true, IMG_TRASH); Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_SPAM)), IDM_DELETE_AS_SPAM, TBT_PUSH, true, IMG_DELETE_SPAM); BtnAttach = Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_ATTACH_FILE)), IDM_ATTACH_FILE, TBT_PUSH, !ReadOnly, IMG_ATTACH_FILE); Commands.Toolbar->AppendSeparator(); BtnReply = Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_REPLY)), IDM_REPLY, TBT_PUSH, ReadOnly, IMG_REPLY); BtnReplyAll = Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_REPLYALL)), IDM_REPLY_ALL, TBT_PUSH, ReadOnly, IMG_REPLY_ALL); BtnForward = Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_FORWARD)), IDM_FORWARD, TBT_PUSH, ReadOnly, IMG_FORWARD); BtnBounce = Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_BOUNCE)), IDM_BOUNCE, TBT_PUSH, ReadOnly, IMG_BOUNCE); Commands.Toolbar->AppendSeparator(); BtnPrev = Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_PREV_MSG)), IDM_PREV_MSG, TBT_PUSH, true, IMG_PREV_ITEM); BtnNext = Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_NEXT_MSG)), IDM_NEXT_MSG, TBT_PUSH, true, IMG_NEXT_ITEM); Commands.Toolbar->AppendSeparator(); Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_HIGH_PRIORITY)), IDM_HIGH_PRIORITY, TBT_TOGGLE, true, IMG_HIGH_PRIORITY); Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_LOW_PRIORITY)), IDM_LOW_PRIORITY, TBT_TOGGLE, true, IMG_LOW_PRIORITY); Commands.Toolbar->AppendSeparator(); Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_READ_RECEIPT)), IDM_READ_RECEIPT, TBT_TOGGLE, true, IMG_READ_RECEIPT); Commands.Toolbar->AppendSeparator(); Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_PRINT)), IDM_PRINT, TBT_PUSH, true, IMG_PRINT); for (LViewI *w: Commands.Toolbar->IterateViews()) MinButY = MAX(MinButY, w->Y()); Commands.Toolbar->Customizable(App->GetOptions(), "MailWindowToolbar"); Commands.SetupCallbacks(App, this, GetItem(), LThingUiToolbar); } // Setup to/from panels LVariant AlwaysShowFrom; if (IsCreated) App->GetOptions()->GetValue(OPT_MailShowFrom, AlwaysShowFrom); bool ShowFrom = !IsCreated && !TestFlag(GetItem()->GetFlags(), MAIL_BOUNCE); LDisplayString Recip(LSysFont, LLoadString(IDS_RECIPIENTS)); LAutoPtr ReplyChk(new LCheckBox(IDC_USE_REPLY_TO, 21, #ifdef MAC 1, #else 6, #endif -1, -1, "Reply To")); int ReplyChkPx = ReplyChk ? ReplyChk->X() : 0; int RECIP_X = 20 + MAX(ReplyChkPx, Recip.X()) + 10; int EditHeight = FontHeight + 6; LVariant HideGpg; if (!item->App->GetOptions()->GetValue(OPT_HideGnuPG, HideGpg) || HideGpg.CastInt32() == 0) { GpgUi = new MailUiGpg( App, this, 21, RECIP_X, !ReadOnly && !TestFlag(GetItem()->GetFlags(), MAIL_SENT)); if (GpgUi) GpgUi->Attach(this); } ToPanel = new LPanel(LLoadString(IDS_TO), FontHeight * 8, !ShowFrom); if (ToPanel) { int Cy = 4; ToPanel->AddView(SetTo = new LCombo(IDC_SET_TO, 20, Cy, 60, EditHeight, 0)); if (SetTo) { SetTo->Insert(LLoadString(IDS_TO)); SetTo->Insert(LLoadString(IDS_CC)); SetTo->Insert(LLoadString(IDS_BCC)); if (SetTo->GetPos().x2 + 10 > RECIP_X) RECIP_X = SetTo->GetPos().x2 + 10; } ToPanel->AddView(Entry = new LEdit(IDC_ENTRY, RECIP_X, Cy, RECIP_SX, EditHeight, "")); Cy = SetTo->GetPos().y2 + 5; ToPanel->AddView(To = new AddressList(App, IDC_TO, RECIP_X, Cy, RECIP_SX, ToPanel->GetOpenSize() - Cy - 6)); if (To) To->SetImageList(App->GetIconImgList(), false); ToPanel->AddView(new LTextLabel(IDC_STATIC, 20, Cy, -1, -1, LLoadString(IDS_RECIPIENTS))); ToPanel->Raised(false); ToPanel->Attach(this); } FromPanel = new LPanel(LLoadString(IDS_FROM), FontHeight + 13, AlwaysShowFrom.CastInt32() || ShowFrom); if (FromPanel) { FromPanel->AddView(new LTextLabel(IDC_STATIC, 21, #ifdef MAC 8, #else 4, #endif -1, -1, LLoadString(IDS_FROM))); if (ShowFrom) { FromPanel->AddView(FromList = new AddressList(App, IDC_FROM, RECIP_X, 2, RECIP_SX, EditHeight)); } else { FromPanel->AddView(FromCbo = new LCombo(IDC_FROM, RECIP_X, 2, RECIP_SX, EditHeight, 0)); } if (IsCreated) { LCheckBox *Chk; auto Label = LLoadString(IDS_ALWAYS_SHOW); FromPanel->AddView(Chk = new LCheckBox(IDC_SHOW_FROM, ADD_X, #ifdef MAC 1, #else 6, #endif -1, -1, Label)); if (Chk) Chk->Value(AlwaysShowFrom.CastInt32()); } FromPanel->Raised(false); FromPanel->Attach(this); } ReplyToPanel = new LPanel("Reply To:", FontHeight + 13, false); if (ReplyToPanel) { ReplyToPanel->Raised(false); ReplyToPanel->AddView(ReplyToChk = ReplyChk.Release()); ReplyToPanel->AddView(ReplyToCbo = new LCombo(IDC_REPLY_TO_ADDR, RECIP_X, 2, RECIP_SX, EditHeight, 0)); ReplyToPanel->Attach(this); } SubjectPanel = new LPanel(LLoadString(IDS_SUBJECT), -(LSysFont->GetHeight() + 16)); if (SubjectPanel) { SubjectPanel->Raised(false); SubjectPanel->AddView( new LTextLabel(IDC_STATIC, 21, 8, -1, -1, LLoadString(IDS_SUBJECT))); SubjectPanel->AddView(Subject = new LEdit(IDC_SUBJECT, RECIP_X, 4, RECIP_SX, EditHeight, "")); SubjectPanel->Attach(this); } CalendarPanel = new LPanel(LLoadString(IDC_CALENDAR), -(LSysFont->GetHeight() + 16), false); if (CalendarPanel) { CalendarPanel->Raised(false); LTableLayout *t = new LTableLayout(IDC_TABLE); if (t) { t->GetCss(true)->Margin(LCss::Len(LCss::LenPx, LTableLayout::CellSpacing)); t->GetCss()->Width(LCss::LenAuto); t->GetCss()->Height(LCss::LenAuto); CalendarPanel->AddView(t); auto c = t->GetCell(0, 0); c->Width(LCss::Len(LCss::LenPx, RECIP_X - (LTableLayout::CellSpacing * 2))); c->PaddingLeft(LCss::Len(LCss::LenPx, 21 - LTableLayout::CellSpacing)); c->Debug = true; c->Add(new LTextLabel(IDC_STATIC, 0, 0, -1, -1, LLoadString(IDS_CALENDAR))); c = t->GetCell(1, 0); c->Add(new LButton(IDC_ADD_CAL_EVENT, 0, 0, -1, -1, LLoadString(IDS_ADD_CAL))); c = t->GetCell(2, 0); c->Add(new LButton(IDC_ADD_CAL_EVENT_POPUP, 0, 0, -1, -1, LLoadString(IDS_ADD_CAL_POPUP))); c = t->GetCell(3, 0); c->Add(CalPanelStatus = new LTextLabel(IDC_STATIC, 0, 0, -1, -1, NULL)); } CalendarPanel->Attach(this); } Tab = new LTabView(IDC_MAIL_UI_TABS, 0, 0, 1000, 1000, 0); if (Tab) { Tab->GetCss(true)->PaddingTop("3px"); // Don't attach text and html controls here, because OnLoad will do it later... TabText = Tab->Append(LLoadString(IDS_TEXT)); TabHtml = Tab->Append("HTML"); TabAttachments = Tab->Append(LLoadString(IDS_ATTACHMENTS)); TabHeader = Tab->Append(LLoadString(IDS_INTERNETHEADER)); if (TabAttachments) { TabAttachments->Append(Attachments = new AttachmentList(IDC_ATTACHMENTS, CONTENT_BORDER, CONTENT_BORDER, 200, 200, this)); if (Attachments) { Attachments->AddColumn(LLoadString(IDS_FILE_NAME), 160); Attachments->AddColumn(LLoadString(IDS_SIZE), 80); Attachments->AddColumn(LLoadString(IDS_MIME_TYPE), 100); Attachments->AddColumn(LLoadString(IDS_CONTENT_ID), 250); } } if (TabHeader) { TabHeader->Append(Header = new LTextView3( IDC_INTERNET_HEADER, CONTENT_BORDER, CONTENT_BORDER, 100, 20)); if (Header) { Header->Sunken(true); } } LTabPage *Fields = Tab->Append(LLoadString(IDS_FIELDS)); if (Fields) { Fields->LoadFromResource(IDD_MAIL_FIELDS); LTableLayout *l; if (GetViewById(IDC_TABLE, l)) { l->SetPourLargest(false); } } Tab->Attach(this); // Colours LColourSelect *Colour; if (GetViewById(IDC_COLOUR, Colour)) { LArray c32; for (int i=0; iSetColourList(&c32); } else LAssert(!"No colour control?"); } PourAll(); if (Commands.Toolbar) { if (ToPanel) ToPanel->SetClosedSize(Commands.Toolbar->Y()-1); if (FromPanel) FromPanel->SetClosedSize(Commands.Toolbar->Y()-1); if (ReplyToPanel) ReplyToPanel->SetClosedSize(Commands.Toolbar->Y()-1); } SetIcon("About64px.png"); OnLoad(); _Running = true; Visible(true); RegisterHook(this, LKeyEvents); LResources::StyleElement(this); } } MailUi::~MailUi() { DeleteObj(TextView); if (Attachments) { Attachments->RemoveAll(); } LOptionsFile *Options = (GetItem() && App) ? App->GetOptions() : 0; if (Options) { LRect p = GetPos(); if (p.x1 >= 0 && p.y1 >= 0) { LVariant v = p.GetStr(); Options->SetValue("MailUI.Pos", v); } } Tab = 0; if (GetItem()) { GetItem()->Ui = NULL; } else LgiTrace("%s:%i - Error: no item to clear UI ptr?\n", _FL); // We delete the LView here because objects // need to have their virtual tables intact _Delete(); } Mail *MailUi::GetItem() { return _Item ? _Item->IsMail() : 0; } void MailUi::SetItem(Mail *m) { if (_Item) { Mail *old = _Item->IsMail(); if (old) old->Ui = NULL; else LAssert(0); _Item = NULL; } if (m) { _Item = m; m->Ui = this; } } bool MailUi::SetDirty(bool Dirty, bool Ui) { bool b = ThingUi::SetDirty(Dirty, Ui); if (MetaFieldsDirty && !Dirty) { Mail *m = GetItem(); if (m) { LColourSelect *Colour; if (GetViewById(IDC_COLOUR, Colour)) { uint32_t Col = (uint32_t)Colour->Value(); m->SetMarkColour(Col); } else LgiTrace("%s:%i - Can't find IDC_COLOUR\n", _FL); auto s = GetCtrlName(IDC_LABEL); m->SetLabel(s); if (m->GetObject()->GetInt(FIELD_STORE_TYPE) != Store3Imap) // Imap knows to save itself. m->SetDirty(); m->Update(); } MetaFieldsDirty = false; } return b; } #define IDM_FILTER_BASE 2000 #define IDM_CHARSET_BASE 3000 void AddActions(LSubMenu *Menu, List &Filters, LArray Folders) { if (!Menu) return; auto StartLen = Menu->Length(); for (auto Folder: Folders) { for (ScribeFolder *f=Folder->GetChildFolder(); f; f=f->GetNextFolder()) { auto Sub = Menu->AppendSub(f->GetName(true)); if (Sub) { auto Item = Sub->GetParent(); if (Item) Item->Icon(ICON_CLOSED_FOLDER); Sub->SetImageList(Menu->GetImageList(), false); f->LoadThings(); AddActions(Sub, Filters, {f}); } } List a; Folder->LoadThings(); for (auto t: Folder->Items) { a.Insert(t->IsFilter()); } a.Sort(FilterCompare); for (auto i: a) { LVariant Name; if (i->GetVariant("Name", Name) && Name.Str()) { char n[256]; strcpy_s(n, sizeof(n), Name.Str()); for (char *s=n; *s; s++) { if (*s == '&') { memmove(s + 1, s, strlen(s)+1); s++; } } auto Item = Menu->AppendItem(n, (int) (IDM_FILTER_BASE + Filters.Length()), true); if (Item) { Item->Icon(ICON_FILTER); Filters.Insert(i); } } } } if (StartLen == Menu->Length()) { char s[64]; sprintf_s(s, sizeof(s), "(%s)", LLoadString(IDS_EMPTY)); Menu->AppendItem(s, 0, false); } } bool MailUi::OnViewKey(LView *v, LKey &k) { if (k.Down() && k.CtrlCmd() && !k.Alt()) { switch (k.vkey) { case LK_RETURN: { PostEvent(M_COMMAND, IDM_SEND_MSG); return true; } case LK_UP: { SeekMsg(-1); return true; } case LK_DOWN: { SeekMsg(1); return true; } } switch (k.c16) { case 'p': case 'P': { App->ThingPrint(NULL, GetItem(), NULL, this); break; } case 'f': case 'F': { if (Tab->Value() == 0) { if (TextView) TextView->DoFind(NULL); } else if (Tab->Value() == 1) { if (HtmlView) HtmlView->DoFind(NULL); } break; } case 'r': case 'R': { OnCommand(IDM_REPLY, 0, NULL); return true; } case 'w': case 'W': { if (OnRequestClose(false)) Quit(); return true; } case 's': case 'S': { if (IsDirty()) { SetDirty(false); } return true; } case 't': case 'T': { Tab->Value(0); if (TextView) TextView->Focus(true); return true; } case 'h': case 'H': { Tab->Value(1); if (HtmlView) HtmlView->Focus(true); return true; } } } return ThingUi::OnViewKey(v, k); } void MailUi::SerializeText(bool FromCtrl) { if (GetItem() && TextView) { if (FromCtrl) { GetItem()->SetBody(TextView->Name()); } else { TextView->Name(GetItem()->GetBody()); } } } const char *FilePart(const char *Uri) { const char *Dir = Uri + strlen(Uri) - 1; while (Dir > Uri && Dir[-1] != '/' && Dir[-1] != '\\') { Dir--; } return Dir; } void MailUi::OnAttachmentsChange() { Attachments->ResizeColumnsToContent(); if (GetItem()) { TabAttachments->GetCss(true)->FontBold(GetItem()->Attachments.Length() > 0); TabAttachments->OnStyleChange(); } } bool MailUi::NeedsCapability(const char *Name, const char *Param) { if (!InThread()) { PostEvent(M_NEEDS_CAP, (LMessage::Param)NewStr(Name)); } else { if (!Name) return false; if (Caps.Find(Name)) return true; Caps.Add(Name, true); char msg[256]; LArray Actions; LAutoPtr Back; if (!_stricmp(Name, "RemoteContent")) { Actions.Add(LLoadString(IDS_ALWAYS_SHOW_REMOTE_CONTENT)); Actions.Add(LLoadString(IDS_SHOW_REMOTE_CONTENT)); Back.Reset(new LColour(L_LOW)); strcpy_s(msg, sizeof(msg), LLoadString ( IDS_REMOTE_CONTENT_MSG, "To protect your privacy Scribe has blocked the remote content in this message." )); } else { Actions.Add(LLoadString(IDS_INSTALL)); int ch = 0; for (auto k : Caps) ch += sprintf_s(msg+ch, sizeof(msg)-ch, "%s%s", ch?", ":"", k.key); ch += sprintf_s(msg+ch, sizeof(msg)-ch, " is required to display this content."); } if (!MissingCaps) { MissingCaps = new MissingCapsBar(this, &Caps, msg, App, Actions, Back); auto c = IterateViews(); auto Idx = c.IndexOf(GpgUi); AddView(MissingCaps, (int)Idx+1); AttachChildren(); OnPosChange(); } else { MissingCaps->SetMsg(msg); } } return true; } void MailUi::OnCloseInstaller() { if (MissingCaps) { DeleteObj(MissingCaps); PourAll(); } } void MailUi::OnInstall(LCapabilityTarget::CapsHash *Caps, bool Status) { } void MailUi::OnChildrenChanged(LViewI *Wnd, bool Attaching) { if (Wnd == (LViewI*)MissingCaps && !Attaching) { MissingCaps = NULL; PourAll(); } } LDocView *MailUi::GetDoc(const char *MimeType) { if (!MimeType) { MimeType = sTextPlain; LVariant v; if (App->GetOptions()->GetValue(OPT_DefaultAlternative, v) && v.CastInt32() > 0) MimeType = sTextHtml; } LAssert(MimeType != NULL); if (!_stricmp(MimeType, sTextPlain)) return TextView; else if (!_stricmp(MimeType, sTextHtml)) return HtmlView; else LAssert(!"Invalid mime type."); return NULL; } bool MailUi::SetDoc(LDocView *v, const char *MimeType) { if (!MimeType) { MimeType = sTextPlain; LVariant v; if (App->GetOptions()->GetValue(OPT_DefaultAlternative, v) && v.CastInt32() > 0) MimeType = sTextHtml; } LAssert(MimeType != NULL); if (!_stricmp(MimeType, sTextPlain)) { LTextView3 *Txt = static_cast(v); if (Txt) { if (Txt != TextView) { DeleteObj(TextView); TextView = Txt; if (TabText && !TextView->IsAttached()) TabText->Append(TextView); } TextLoaded = true; if (_Running) TabText->Select(); } else { LAssert(!"Invalid ctrl."); return false; } } else if (!_stricmp(MimeType, sTextHtml)) { LDocView *Html = dynamic_cast(v); if (Html) { if (Html != HtmlView) { DeleteObj(HtmlView); HtmlView = Html; LCapabilityClient *cc = dynamic_cast(v); if (cc) cc->Register(this); if (TabHtml && !HtmlView->IsAttached()) TabHtml->Append(HtmlView); } HtmlLoaded = true; if (_Running) TabHtml->Select(); } else { LAssert(!"Invalid ctrl."); return false; } } else { LAssert(!"Invalid mime type."); return false; } return true; } bool MailUi::IsWorking(int Set) { if (!GetItem()) return false; // Are any of the attachments busy doing something? LDataI *AttachPoint = GetItem() && Set >= 0 ? GetItem()->GetFileAttachPoint() : NULL; List Attachments; if (GetItem()->GetAttachments(&Attachments)) { // Attachment *Match = NULL; for (auto a: Attachments) { if (Set >= 0) { a->SetIsResizing(Set != 0); if (AttachPoint) { a->GetObject()->Save(AttachPoint); } } else if (a->GetIsResizing()) { return true; } } } // Check rich text control as well... auto Rte = dynamic_cast(HtmlView); if (Rte) { if (Rte->IsBusy()) { return true; } } return false; } class BusyPanel : public LPanel { public: BusyPanel() : LPanel("Working...", LSysFont->GetHeight() << 1) { LViewI *v; LCss::ColorDef Bk(LColour(255, 128, 0)); GetCss(true)->BackgroundColor(Bk); AddView(v = new LTextLabel(IDC_STATIC, 30, 5, -1, -1, "Still resizing images...")); v->GetCss(true)->BackgroundColor(Bk); AddView(v = new LButton(IDCANCEL, 200, 3, -1, -1, LLoadString(IDS_CANCEL))); v->GetCss(true)->NoPaintColor(Bk); } }; void MailUi::SetCmdAfterResize(int Cmd) { if (CmdAfterResize == 0) { CmdAfterResize = Cmd; if (!WorkingDlg) { WorkingDlg = new BusyPanel; if (WorkingDlg) { AddView(WorkingDlg, 1); AttachChildren(); OnPosChange(); } } } } bool MailUi::OnRequestClose(bool OsClose) { bool Working = IsWorking(); if (Working) { SetCmdAfterResize(IDM_SAVE_CLOSE); return false; } return ThingUi::OnRequestClose(OsClose); } void MailUi::OnChange() { if (!IsDirty()) OnLoad(); } struct MailUiNameAddr { LString Name, Addr; MailUiNameAddr(const char *name = 0, const char *addr = 0) { Name = name; Addr = addr; } }; void MailUi::OnLoad() { bool Edit = false; bool ReadOnly = true; Mail *Item = GetItem(); _Running = false; if (Item && Item->App) { Edit = TestFlag(Item->GetFlags(), MAIL_CREATED); ReadOnly = !TestFlag(Item->GetFlags(), MAIL_CREATED | MAIL_BOUNCE); if (Entry) { Entry->Name(""); } if (To) { To->OnInit(Item->GetTo()); } if (FromCbo) { LHashTbl, MailUiNameAddr*> ReplyToAddrs; const char *Template = "%s <%s>"; FromCbo->Empty(); int Idx = -1; LVariant DefName, DefAddr; // LOptionsFile *Opts = Item->Window->GetOptions(); for (auto a : *Item->App->GetAccounts()) { LVariant Name = a->Identity.Name(); LVariant Addr = a->Identity.Email(); LVariant ReplyTo = a->Identity.ReplyTo(); if (!a->IsValid() || a->Send.Disabled()) continue; if (ReplyTo.Str()) { if (!ReplyToAddrs.Find(ReplyTo.Str())) ReplyToAddrs.Add(ReplyTo.Str(), new MailUiNameAddr(Name.Str(), ReplyTo.Str())); } else if (Addr.Str()) { if (!ReplyToAddrs.Find(Addr.Str())) ReplyToAddrs.Add(Addr.Str(), new MailUiNameAddr(Name.Str(), Addr.Str())); } if (Name.Str() && Addr.Str()) { if (!DefAddr.Str() || _stricmp(DefAddr.Str(), Addr.Str())) { auto FromAddr = Item->GetFromStr(FIELD_EMAIL); if (FromAddr && _stricmp(Addr.Str(), FromAddr) == 0) { Idx = (int)FromCbo->Length(); } LString p; p.Printf(Template, Name.Str(), Addr.Str()); int Id = a->Receive.Id(); int CurLen = (int)FromCbo->Length(); LAssert(Id != 0); FromAccountId[CurLen] = Id; FromCbo->Insert(p); } } } if (Idx < 0) { auto FromName = Item->GetFromStr(FIELD_NAME); auto FromAddr = Item->GetFromStr(FIELD_EMAIL); if (FromAddr) { LStringPipe p; if (FromName) p.Print(Template, FromName, FromAddr); else p.Print("<%s>", FromAddr); LAutoString s(p.NewStr()); FromAccountId[FromCbo->Length()] = -1; Idx = (int)FromCbo->Length(); FromCbo->Insert(s); FromCbo->Value(Idx); } } else { FromCbo->Value(Idx); } if (ReplyToAddrs.Length() > 0 && ReplyToCbo) { auto CurAddr = Item->GetReply() ? Item->GetReply()->GetStr(FIELD_EMAIL) : NULL; int CurIdx = -1; // for (MailUiNameAddr *na = ReplyToAddrs.First(); na; na = ReplyToAddrs.Next()) for (auto na : ReplyToAddrs) { char s[256]; sprintf_s(s, sizeof(s), Template, na.value->Name.Get(), na.value->Addr.Get()); if (CurAddr && !_stricmp(na.value->Addr, CurAddr)) CurIdx = (int)ReplyToCbo->Length(); ReplyToCbo->Insert(s); } if (CurIdx >= 0) { ReplyToCbo->Value(CurIdx); ReplyToChk->Value(true); } else { ReplyToChk->Value(false); } } ReplyToAddrs.DeleteObjects(); } else if (FromList) { FromList->Empty(); ListAddr *na = new ListAddr(App, Item->GetFrom()); if (na) { na->CC = MAIL_ADDR_FROM; na->OnFind(); FromList->Insert(na); } } if (Subject) { Subject->Name(Item->GetSubject()); char Title[140]; auto Subj = Item->GetSubject(); if (Subj) sprintf_s(Title, sizeof(Title), "%s - %.100s", LLoadString(IDS_MAIL_MESSAGE), Subj); else sprintf_s(Title, sizeof(Title), "%s", LLoadString(IDS_MAIL_MESSAGE)); Title[sizeof(Title)-1] = 0; for (char *s = Title; *s; s++) if (*s == '\n' || *s == '\r') *s = ' '; Name(Title); } int64_t Rgb32 = Item->GetMarkColour(); SetCtrlValue(IDC_COLOUR, Rgb32 > 0 ? Rgb32 : -1); SetCtrlName(IDC_LABEL, Item->GetLabel()); char Date[256]; Item->GetDateReceived()->Get(Date, sizeof(Date)); SetCtrlName(IDC_RECEIVED_DATE, Date); Item->GetDateSent()->Get(Date, sizeof(Date)); SetCtrlName(IDC_SENT_DATE, Date); TextLoaded = false; HtmlLoaded = false; auto TextContent = Item->GetBody(); auto HtmlContent = Item->GetHtml(); Sx = Sy = -1; LDocView *DocView = NULL; if (TabText && (DocView = Item->CreateView(this, sTextPlain, true, -1, !Edit))) { if (DocView == HtmlView) { // CreateView converted the text to HTML to embed Emojis. If we have // actual HTML content it'll overwrite the text portion, so we need // to move the HTML control to the text tab to leave room for actual HTML. DeleteObj(TextView); TextView = HtmlView; HtmlView = NULL; TextView->Detach(); TabText->Append(TextView); TextLoaded = true; HtmlLoaded = false; } if (!TextView && Edit) { // What the? Force creation of control... LAssert(!"Must have an edit control."); LDocView *Dv = App->CreateTextControl(IDC_TEXT_VIEW, sTextPlain, Edit, GetItem()); if (Dv) SetDoc(Dv, sTextPlain); LAssert(TextView != NULL); } if (TextView) { TextView->Visible(true); // This needs to be below the resize of the control so that // any wrapping has already been done and thus the scroll // bar is laid out already. if (Item->Cursor > 0) TextView->SetCaret(Item->Cursor, false); TabText->GetCss(true)->FontBold(ValidStr(TextContent)); TabText->OnStyleChange(); } } bool ValidHtml = ValidStr(HtmlContent); if (TabHtml && Item->CreateView(this, sTextHtml, true, -1, !Edit) && HtmlView) { HtmlView->Visible(true); if (Item->Cursor > 0) HtmlView->SetCaret(Item->Cursor, false); TabHtml->GetCss(true)->FontBold(ValidHtml); TabHtml->OnStyleChange(); } LVariant DefTab; Item->App->GetOptions()->GetValue(Edit ? OPT_EditControl : OPT_DefaultAlternative, DefTab); CurrentEditCtrl = (Edit || ValidHtml) && DefTab.CastInt32(); Tab->Value(CurrentEditCtrl); HtmlCtrlDirty = !ValidHtml; TextCtrlDirty = !ValidStr(TextContent); if (CalendarPanel) { auto CalEvents = GetItem()->GetCalendarAttachments(); CalendarPanel->Open(CalEvents.Length() > 0); } OnPosChange(); if (Attachments) { Attachments->RemoveAll(); List Files; if (Item->GetAttachments(&Files)) { for (auto a: Files) Attachments->Insert(a); } OnAttachmentsChange(); } if (Header) { LAutoString Utf((char*)LNewConvertCp("utf-8", Item->GetInternetHeader(), "iso-8859-1")); Header->Name(Utf); Header->SetEnv(Item); } bool Update = (Item->GetFlags() & MAIL_READ) == 0 && (Item->GetFlags() & MAIL_CREATED) == 0; if (Update) { Item->SetFlags(Item->GetFlags() | MAIL_READ); } if (Commands.Toolbar) { int p = Item->GetPriority(); Commands.Toolbar->SetCtrlValue(IDM_HIGH_PRIORITY, p < MAIL_PRIORITY_NORMAL); Commands.Toolbar->SetCtrlValue(IDM_LOW_PRIORITY, p > MAIL_PRIORITY_NORMAL); Commands.Toolbar->SetCtrlValue(IDM_READ_RECEIPT, TestFlag(Item->GetFlags(), MAIL_READ_RECEIPT)); } } if (Item->GetFlags() & (MAIL_CREATED | MAIL_BOUNCE)) { if (Entry) Entry->Focus(true); } else { if (TextView) TextView->Focus(true); } if (BtnPrev && BtnNext) { if (Item && Container) { /* int Items = Item->GetList()->Length(); int i = Item->GetList()->IndexOf(Item); */ auto Items = Container->Length(); auto i = Container->IndexOf(Item); BtnPrev->Enabled(i < (ssize_t)Items - 1); BtnNext->Enabled(i > 0); } else { BtnPrev->Enabled(false); BtnNext->Enabled(false); } } if (BtnSend) BtnSend->Enabled(!ReadOnly); if (BtnSave) BtnSave->Enabled(!ReadOnly); if (BtnSaveClose) BtnSaveClose->Enabled(!ReadOnly); if (BtnAttach) BtnAttach->Enabled(!ReadOnly); if (BtnReply) BtnReply->Enabled(ReadOnly); if (BtnReplyAll) BtnReplyAll->Enabled(ReadOnly); if (BtnForward) BtnForward->Enabled(true); if (BtnBounce) BtnBounce->Enabled(ReadOnly); if (Commands.Toolbar) { Commands.Toolbar->SetCtrlEnabled(IDM_HIGH_PRIORITY, Edit); Commands.Toolbar->SetCtrlEnabled(IDM_LOW_PRIORITY, Edit); Commands.Toolbar->SetCtrlEnabled(IDM_READ_RECEIPT, Edit); } _Running = true; } void MailUi::OnSave() { if (!GetItem()) return; if (GetItem()->GetFlags() & MAIL_SENT) { // Save a copy instead of over writing the original sent email Mail *Copy = new Mail(App, GetItem()->GetObject()->GetStore()->Create(MAGIC_MAIL)); if (Copy) { *Copy = *_Item; Copy->SetFlags(MAIL_READ | MAIL_CREATED, true); Copy->SetFolder(GetItem()->GetFolder()); Copy->SetDateSent(0); SetItem(Copy); } } Mail *Item = GetItem(); if (To) { To->OnSave(Item->GetObject()->GetStore(), Item->GetTo()); } LMailStore *AccountMailStore = NULL; if (FromCbo && Item->GetFrom() && FromAccountId.Length() > 0) { int64 CboVal = FromCbo->Value(); LAssert(CboVal < (ssize_t)FromAccountId.Length()); int AccountId = FromAccountId[(int)CboVal]; LDataPropI *Frm = Item->GetFrom(); if (AccountId < 0) { // From is a literal address, not an account ID. This can happen when bouncing email. Mailto mt(App, FromCbo->Name()); if (mt.To.Length() == 1) { AddressDescriptor *a = mt.To[0]; if (a) { Frm->SetStr(FIELD_NAME, a->sName); Frm->SetStr(FIELD_EMAIL, a->sAddr); } } else LAssert(0); } else if (AccountId > 0) { ScribeAccount *a = Item->App->GetAccountById(AccountId); if (a) { Frm->SetStr(FIELD_NAME, a->Identity.Name().Str()); Frm->SetStr(FIELD_EMAIL, a->Identity.Email().Str()); } else LAssert(!"From account missing."); // Find the associated mail store for this account. Hopefully we can put any new // mail into the mail store that the account is using. // // Check for IMAP mail store? AccountMailStore = a->Receive.GetMailStore(); if (!AccountMailStore) { // Nope... what about a receive path? LVariant DestFolder = a->Receive.DestinationFolder(); if (ValidStr(DestFolder.Str())) { AccountMailStore = Item->App->GetMailStoreForPath(DestFolder.Str()); } } } else LAssert(!"No account id."); } LDataPropI *ReplyObj = Item->GetReply(); if (ReplyToCbo != NULL && ReplyObj != NULL && ReplyToChk != NULL && ReplyToChk->Value()) { Mailto mt(App, ReplyToCbo->Name()); if (mt.To.Length() == 1) { AddressDescriptor *a = mt.To[0]; if (a && a->sAddr) { if (a->sName) ReplyObj->SetStr(FIELD_NAME, a->sName); ReplyObj->SetStr(FIELD_EMAIL, a->sAddr); } } } Item->SetSubject(Subject->Name()); Item->SetLabel(GetCtrlName(IDC_LABEL)); auto c32 = GetCtrlValue(IDC_COLOUR); Item->SetMarkColour(c32); LDocView *Ctrl = CurrentEditCtrl ? HtmlView : TextView; if (Ctrl) { // Delete all existing data... Item->SetBody(0); Item->SetBodyCharset(0); Item->SetHtml(0); Item->SetHtmlCharset(0); const char *MimeType = Ctrl->GetMimeType(); const char *Charset = Ctrl->GetCharset(); if (!_stricmp(MimeType, sTextHtml)) { LArray Media; // Set the HTML part LString HtmlFormat; if (!Ctrl->GetFormattedContent("text/html", HtmlFormat, &Media)) HtmlFormat = Ctrl->Name(); Item->SetHtml(HtmlFormat); Item->SetHtmlCharset(Charset); // Also set a text version for the alternate LString TxtFormat; if (!Ctrl->GetFormattedContent(sTextPlain, TxtFormat)) { TxtFormat = HtmlToText(Item->GetHtml(), Charset); } if (TxtFormat) { Item->SetBody(TxtFormat); Item->SetBodyCharset(Charset); } auto Obj = Item->GetObject(); // This clears any existing multipart/related objects... Obj->SetObj(FIELD_HTML_RELATED, NULL); if (Media.Length() > 0) { // Make a table of existing attachments so that we don't duplicate // these new ones. LArray Objs; LHashTbl,LDataI*> Map; if (GetItem()->GetAttachmentObjs(Objs)) { for (auto i : Objs) { auto Cid = i->GetStr(FIELD_CONTENT_ID); if (Cid) Map.Add(Cid, i); } } // If there are media attachments, splice them into the MIME tree. // This should go after setting the text part so that the right // MIME alternative structure is generated. auto Store = Obj->GetStore(); for (auto &Cm : Media) { LDataI *a = Store->Create(MAGIC_ATTACHMENT); if (a) { LAssert(Cm.Valid()); auto Existing = Map.Find(Cm.Id); if (Existing) { // Delete the existing attachment Thing *t = CastThing(Existing); Attachment *a = t ? t->IsAttachment() : NULL; if (a) { // Delete both the Attachment and it's store object... auto it = GetItem(); it->DeleteAttachment(a); } else { // There is Attachment object for the LDataI.... but we can // still delete it from the store. LArray del; del.Add(Existing); Existing->GetStore()->Delete(del, false); } } LgiTrace("Adding related: %s %s " LPrintfInt64 "\n", Cm.FileName.Get(), Cm.MimeType.Get(), Cm.Stream->GetSize()); a->SetStr(FIELD_CONTENT_ID, Cm.Id); a->SetStr(FIELD_NAME, Cm.FileName); a->SetStr(FIELD_MIME_TYPE, Cm.MimeType); a->SetStream(Cm.Stream); Obj->SetObj(FIELD_HTML_RELATED, a); } } } } else { auto Text = Ctrl->Name(); Item->SetBody(Text); Item->SetBodyCharset(Charset); } } #if SAVE_HEADERS char *Headers = Header ? Header->Name() : 0; if (Headers) { DeleteArray(Item->InternetHeader); Item->InternetHeader = NewStr(Headers); } #endif Item->GetMessageId(true); Item->CreateMailHeaders(); Item->Update(); ScribeFolder *Folder = Item->GetFolder(); // Now get the associated outbox for this mail ScribeFolder *Outbox = Item->App->GetFolder(FOLDER_OUTBOX, AccountMailStore); auto Fld = Folder ? Folder : Outbox; LAssert(Fld != NULL); bool Status = Fld ? Item->Save(Fld) : false; if (Status) { LArray c; c.Add(Item->GetObject()); Item->App->SetContext(_FL); Item->App->OnChange(c, 0); } } bool MailUi::AddRecipient(AddressDescriptor *Addr) { if (Addr && To) { ListAddr *La = dynamic_cast(Addr); if (La) { To->Insert(La); return true; } } return false; } bool MailUi::AddRecipient(Contact *c) { ListAddr *La = new ListAddr(c); if (La) { To->Insert(La); return true; } return false; } bool MailUi::AddRecipient(const char *Email, const char *Name) { ListAddr *La = new ListAddr(App, Email, Name); if (La) { To->Insert(La); return true; } return false; } bool MailUi::SeekMsg(int delta) { bool Status = false; if (Container) { Mail *Item = GetItem(); auto Index = Container->IndexOf(Item); Mail *Next = Index >= 0 ? (*Container)[Index + delta] : 0; if (Next) { SetDirty(false); // called OnSave() if necessary _Running = false; if (Item->Ui) { // close any existing user interface if (Item->Ui != this) { Item->Ui->Quit(); } else { Item->Ui = 0; } } if (Header) Header->SetEnv(0); Caps.Empty(); SetItem(Next); Item = GetItem(); // select this item in the list if (Item->GetList()) { Item->GetList()->Select(Item); Item->ScrollTo(); } Status = true; OnLoad(); _Running = true; } } return Status; } int MailUi::HandleCmd(int Cmd) { switch (Cmd) { case IDM_READ_RECEIPT: { if (Commands.Toolbar) { int f = GetItem()->GetFlags(); if (Commands.Toolbar->GetCtrlValue(IDM_READ_RECEIPT)) { SetFlag(f, MAIL_READ_RECEIPT); } else { ClearFlag(f, MAIL_READ_RECEIPT); } GetItem()->SetFlags(f); } break; } case IDM_HIGH_PRIORITY: { if (Commands.Toolbar) { GetItem()->SetPriority(Commands.Toolbar->GetCtrlValue(IDM_HIGH_PRIORITY) ? MAIL_PRIORITY_HIGH : MAIL_PRIORITY_NORMAL); SetDirty(true); Commands.Toolbar->SetCtrlValue(IDM_HIGH_PRIORITY, GetItem()->GetPriority() < MAIL_PRIORITY_NORMAL); Commands.Toolbar->SetCtrlValue(IDM_LOW_PRIORITY, GetItem()->GetPriority() > MAIL_PRIORITY_NORMAL); } break; } case IDM_LOW_PRIORITY: { if (Commands.Toolbar) { GetItem()->SetPriority(Commands.Toolbar->GetCtrlValue(IDM_LOW_PRIORITY) ? MAIL_PRIORITY_LOW : MAIL_PRIORITY_NORMAL); SetDirty(true); Commands.Toolbar->SetCtrlValue(IDM_HIGH_PRIORITY, GetItem()->GetPriority() < MAIL_PRIORITY_NORMAL); Commands.Toolbar->SetCtrlValue(IDM_LOW_PRIORITY, GetItem()->GetPriority() > MAIL_PRIORITY_NORMAL); } break; } case IDM_PREV_MSG: { SeekMsg(1); break; } case IDM_NEXT_MSG: { SeekMsg(-1); break; } case IDM_SEND_MSG: { bool Working = IsWorking(); if (Working) { SetCmdAfterResize(Cmd); break; } if (!GetItem() || !App) { LAssert(!"Missing item or window ptr."); break; } // Normal save bool IsInPublicFolder = GetItem()->GetFolder() && GetItem()->GetFolder()->IsPublicFolders(); if (IsInPublicFolder) { auto i = GetItem(); LDateTime n; n.SetNow(); i->SetDateSent(&n); i->Update(); } OnDataEntered(); OnSave(); SetDirty(false, false); GetItem()->Send(true); Quit(); return 0; } case IDM_DELETE_MSG: { LVariant ConfirmDelete, DelDirection; App->GetOptions()->GetValue(OPT_ConfirmDelete, ConfirmDelete); App->GetOptions()->GetValue(OPT_DelDirection, DelDirection); int WinAction = DelDirection.CastInt32() - 1; // -1 == Next, 0 == Close, 1 == Prev if (!ConfirmDelete.CastInt32() || LgiMsg(this, LLoadString(IDS_DELETE_ASK), AppName, MB_YESNO) == IDYES) { Mail *Del = GetItem()->GetObject() ? GetItem() : 0; if (Del) { if (!Del->GetObject()->IsOnDisk()) Del = 0; } if (!WinAction || !SeekMsg(WinAction)) { SetItem(0); PostEvent(M_CLOSE); } if (Del && Del->GetObject()) { Del->OnDelete(); } } break; } case IDM_DELETE_AS_SPAM: { LVariant DelDirection; App->GetOptions()->GetValue(OPT_DelDirection, DelDirection); int WinAction = DelDirection.CastInt32() - 1; // -1 == Next, 0 == Close, 1 == Prev if (GetItem()) { Mail *Del = GetItem()->GetObject() ? GetItem() : 0; if (!WinAction || !SeekMsg(WinAction)) { SetItem(0); PostEvent(M_CLOSE); } if (Del) { Del->DeleteAsSpam(this); } } break; } case IDM_SAVE: { OnDataEntered(); SetDirty(false, false); break; } case IDM_SAVE_CLOSE: { bool Working = IsWorking(); if (Working) { SetCmdAfterResize(Cmd); break; } OnDataEntered(); SetDirty(false, false); // fall thru } case IDM_CLOSE: { Quit(); return 0; } case IDM_REPLY: case IDM_REPLY_ALL: { App->MailReplyTo(GetItem(), Cmd == IDM_REPLY_ALL); SetDirty(false); PostEvent(M_CLOSE); break; } case IDM_FORWARD: { App->MailForward(GetItem()); if (IsDirty()) OnSave(); PostEvent(M_CLOSE); break; } case IDM_BOUNCE: { App->MailBounce(GetItem()); OnSave(); PostEvent(M_CLOSE); break; } case IDM_PRINT: { if (GetItem() && App) { if (IsDirty()) { OnDataEntered(); OnSave(); } App->ThingPrint(NULL, GetItem(), 0, this); } break; } case IDM_ATTACH_FILE: { auto Select = new LFileSelect(this); Select->MultiSelect(true); Select->Type("All files", LGI_ALL_FILES); Select->Open([this](auto dlg, auto status) { if (status) { Mail *m = GetItem(); if (m) { for (size_t i=0; iLength(); i++) { char File[MAX_PATH_LEN]; if (!LResolveShortcut((*dlg)[i], File, sizeof(File))) { strcpy_s(File, sizeof(File), (*dlg)[i]); } Attachment *a = m->AttachFile(this, File); if (a && Attachments) { Attachments->Insert(a); Attachments->ResizeColumnsToContent(); } } } } delete dlg; }); break; } default: { if (Commands.ExecuteCallbacks(App, this, GetItem(), Cmd)) return true; return false; } } return true; } int MailUi::OnCommand(int Cmd, int Event, OsView From) { if (GpgUi) { GpgUi->DoCommand(Cmd, [this, Cmd](auto r) { if (!r) HandleCmd(Cmd); }); } else { HandleCmd(Cmd); } return LWindow::OnCommand(Cmd, Event, From); } LArray Mail::GetCalendarAttachments() { List Attachments; if (!GetAttachments(&Attachments)) return false; LArray Cal; for (auto a: Attachments) { LString Mt = a->GetMimeType(); if (Mt.Equals("text/calendar")) Cal.Add(a); } return Cal; } bool MailUi::AddCalendarEvent(bool AddPopupReminder) { LString Msg; auto Result = GetItem()->AddCalendarEvent(this, AddPopupReminder, &Msg); auto css = CalPanelStatus->GetCss(true); if (Result) css->Color(LCss::ColorInherit); else css->Color(LColour::Red); CalPanelStatus->Name(Msg); return Result; } bool Mail::AddCalendarEvent(LViewI *Parent, bool AddPopupReminder, LString *Msg) { LString Err, s; auto Cal = GetCalendarAttachments(); int NewEvents = 0, DupeEvents = 0, Processed = 0, Cancelled = 0, NotMatched = 0; ScribeFolder *Folder = NULL; if (Cal.Length() == 0) { Err = "There are no attached events to add."; goto OnError; } Folder = App->GetFolder(FOLDER_CALENDAR); if (!Folder) { Err = "There no calendar folder to save to."; goto OnError; } for (auto a: Cal) { auto Event = App->CreateThingOfType(MAGIC_CALENDAR); if (Event) { LString Mt = a->GetMimeType(); LAutoPtr Data(a->GotoObject(_FL)); if (Data) { if (Event->Import(AutoCast(Data), Mt)) { auto c = Event->IsCalendar(); auto obj = c ? c->GetObject() : NULL; if (!obj) continue; if (AddPopupReminder) { LString s; s.Printf("%g,%i,%i,", 10.0, CalMinutes, CalPopup); obj->SetStr(FIELD_CAL_REMINDERS, s); } // Is it a cancellation? auto Status = obj->GetStr(FIELD_CAL_STATUS); auto IsCancel = Stristr(Status, "CANCELLED") != NULL; if (!IsCancel) { // Does the folder already have a copy of this event? bool AlreadyAdded = false; for (auto t: Folder->Items) { auto Obj = t->IsCalendar(); if (Obj && *Obj == *c) { AlreadyAdded = true; break; } } if (AlreadyAdded) { DupeEvents++; } else { // Write the event to the folder auto Status = Folder->WriteThing(Event); if (Status > Store3Error) { NewEvents++; Event = NULL; } } } else { // Cancellation processing auto Uid = obj->GetStr(FIELD_UID); Thing *Match = NULL; for (auto t: Folder->Items) { auto tCal = t->IsCalendar(); if (tCal && tCal->GetObject()) { auto tUid = tCal->GetObject()->GetStr(FIELD_UID); if (!Stricmp(Uid, tUid)) { Match = t; break; } } } if (Match) { if (!Parent || LgiMsg(Parent, "Delete cancelled event?", "Calendar", MB_YESNO) == IDYES) { auto f = Match->GetFolder(); LArray items; items.Add(Match); f->Delete(items, true); Cancelled++; } } else NotMatched++; } } else LgiTrace("%s:%i - vCal event import failed.\n", _FL); } else LgiTrace("%s:%i - GotoObject failed.\n", _FL); if (Event) Event->DecRef(); } else LgiTrace("%s:%i - CreateThingOfType failed.\n", _FL); } Processed = NewEvents + DupeEvents; if (Processed != Cal.Length()) { Err.Printf("There were errors processing %i events, check the console.", (int)Cal.Length() - Processed); goto OnError; } if (NewEvents || DupeEvents) s.Printf("%i new events, %i duplicates.", NewEvents, DupeEvents); else s.Printf("%i events cancelled, %i not matched.", Cancelled, NotMatched); if (Msg) *Msg = s; if (Processed > 0) { for (auto v: CalendarView::CalendarViews) v->OnContentsChanged(); } return true; OnError: if (Msg) *Msg = Err; return false; } int MailUi::OnNotify(LViewI *Col, LNotification n) { if (dynamic_cast(Col)) { Sx = Sy = -1; OnPosChange(); return 0; } if (GpgUi) { if (n.Type == LNotifyItemDelete && Col == (LViewI*)GpgUi) { GpgUi = NULL; } else { int r = GpgUi->OnNotify(Col, n); if (r) return r; } } int CtrlId = Col->GetId(); switch (CtrlId) { case IDC_MAIL_UI_TABS: { Mail *Item = GetItem(); if (!Item) break; // bool Edit = TestFlag(Item->GetFlags(), MAIL_CREATED); if (n.Type == LNotifyValueChanged) { switch (Col->Value()) { case 0: // Text tab { if (!TextLoaded) { Item->CreateView(this, sTextPlain, true, -1); OnPosChange(); } if (CurrentEditCtrl == 1) { if (HtmlView && TextView && TextCtrlDirty) { // Convert HTML to Text here... TextCtrlDirty = false; auto Html = HtmlView->Name(); if (Html) { LString Txt = HtmlToText(Html, HtmlView->GetCharset()); if (Txt) TextView->Name(Txt); } } CurrentEditCtrl = 0; } break; } case 1: // Html tab { if (!HtmlLoaded) { Item->CreateView(this, sTextHtml, true, -1); OnPosChange(); } if (CurrentEditCtrl == 0) { if (HtmlView && TextView && HtmlCtrlDirty) { // Convert Text to HTML here... HtmlCtrlDirty = false; auto Text = TextView->Name(); if (Text) { LString Html = TextToHtml(Text, TextView->GetCharset()); if (Html) HtmlView->Name(Html); } } CurrentEditCtrl = 1; } break; } default: // Do nothing on other tabs.. break; } } else if (n.Type == LNotifyItemClick) { LMouse m; if (!Col->GetMouse(m)) break; int TabIdx = Tab->HitTest(m); if (TabIdx == 0 || TabIdx == 1) { if (Item && m.IsContextMenu()) { LSubMenu s; s.AppendItem(LLoadString(IDS_DELETE), IDM_DELETE); m.ToScreen(); int Cmd = s.Float(this, m.x, m.y, false); if (Cmd == IDM_DELETE) { if (TabIdx == 0) // Txt { Item->SetBody(NULL); Item->SetBodyCharset(NULL); TextView->Name(NULL); if (TabText) { TabText->GetCss(true)->FontBold(false); TabText->OnStyleChange(); } TextCtrlDirty = false; } else // HTML { Item->SetHtml(NULL); Item->SetHtmlCharset(NULL); HtmlView->Name(NULL); if (TabHtml) { TabHtml->GetCss(true)->FontBold(false); TabHtml->OnStyleChange(); } HtmlCtrlDirty = false; } } } } } break; } case IDC_FROM: { if (_Running && FromCbo && n.Type == LNotifyValueChanged) SetDirty(true); break; } case IDC_SHOW_FROM: { LVariant Show = Col->Value();; App->GetOptions()->SetValue(OPT_MailShowFrom, Show); break; } case IDC_LAUNCH_HTML: { char File[MAX_PATH_LEN]; if (GetItem() && GetItem()->WriteAlternateHtml(File)) { LExecute(File); } break; } case IDC_ENTRY: { if (Entry) { if (ValidStr(Entry->Name())) { if (!Browse) { Browse = new AddressBrowse(App, Entry, To, SetTo); } } if (Browse) { Browse->OnNotify(Entry, n); } if (n.Type == LNotifyReturnKey) { OnDataEntered(); } } break; } case IDC_SET_TO: { if (SetTo) { AddMode = (int)SetTo->Value(); } break; } case IDC_SEND: { OnCommand(IDM_SEND_MSG, 0, #if LGI_VIEW_HANDLE Col->Handle() #else (OsView)NULL #endif ); break; } #if SAVE_HEADERS case IDC_INTERNET_HEADER: #endif case IDC_TEXT_VIEW: case IDC_HTML_VIEW: { Mail *Item = GetItem(); if (!Item) break; bool Edit = TestFlag(Item->GetFlags(), MAIL_CREATED); if ( ( n.Type == LNotifyDocChanged || n.Type == LNotifyCharsetChanged || n.Type == LNotifyFixedWidthChanged || (!IgnoreShowImgNotify && n.Type == LNotifyShowImagesChanged) ) && _Running ) { if (GetItem()) GetItem()->OnNotify(Col, n); SetDirty(true); if (Edit) { if (CtrlId == IDC_TEXT_VIEW) { CurrentEditCtrl = 0; HtmlCtrlDirty = true; TabText->GetCss(true)->FontBold(true); TabText->OnStyleChange(); } else if (CtrlId == IDC_HTML_VIEW) { CurrentEditCtrl = 1; TextCtrlDirty = true; TabHtml->GetCss(true)->FontBold(true); TabHtml->OnStyleChange(); } // LgiTrace("%s:%i - OnNotify: TextLoaded=%i, HtmlLoaded=%i\n", _FL, TextLoaded, HtmlLoaded); } } break; } case IDC_ATTACHMENTS: { if (n.Type == LNotifyItemInsert || n.Type == LNotifyItemDelete) { // fall thru } else { break; } } case IDC_TO: { if ( _Running && ( n.Type == LNotifyItemInsert || n.Type == LNotifyItemDelete || n.Type == LNotifyItemChange ) ) { SetDirty(true); } break; } case IDC_COLOUR: { if (_Running && GetItem()) { MetaFieldsDirty = true; OnDirty(true); } break; } case IDC_LABEL: { if (_Running) { MetaFieldsDirty = true; OnDirty(true); } break; } case IDC_SUBJECT: { if (_Running) { SetDirty(true); } break; } case IDCANCEL: { if (CmdAfterResize) { int Cmd = CmdAfterResize; CmdAfterResize = 0; IsWorking(false); OnCommand(Cmd, 0, NULL); } break; } case IDC_ADD_CAL_EVENT: { AddCalendarEvent(false); break; } case IDC_ADD_CAL_EVENT_POPUP: { AddCalendarEvent(true); break; } } return 0; } void MailUi::OnDataEntered() { auto Name = Entry->Name(); if (ValidStr(Name)) { List New; // Decode the entries Mailto mt(App, Name); New = mt.To; mt.To.Empty(); if (mt.Subject && !ValidStr(GetCtrlName(IDC_SUBJECT))) { SetCtrlName(IDC_SUBJECT, mt.Subject); } // Add the new entries List Cache; App->GetContacts(Cache); AddressDescriptor *ad; while ((ad = New[0])) { New.Delete(ad); ListAddr *t = dynamic_cast(ad); if (t) { t->CC = (EmailAddressType)GetCtrlValue(IDC_SET_TO); t->OnFind(&Cache); To->Insert(t, 0); } else { DeleteObj(ad); } } // Clear the entry box for the next one Entry->Name(""); Entry->Select(-1, -1); } } void MailUi::OnPosChange() { LWindow::OnPosChange(); if (Tab && (Sx != X() || Sy != Y())) { Sx = X(); Sy = Y(); LRect r = Tab->GetCurrent()->GetClient(); r.Inset(CONTENT_BORDER, CONTENT_BORDER); if (TextView) TextView->SetPos(r, true); if (HtmlView) HtmlView->SetPos(r, true); if (Attachments) Attachments->SetPos(r, true); if (Header) Header->SetPos(r, true); } } void MailUi::OnPaint(LSurface *pDC) { LCssTools Tools(this); Tools.PaintContent(pDC, GetClient()); } void MailUi::OnPulse() { if (IsDirty() && TextView && GetItem()) { // Ui -> Object OnSave(); // Object -> Disk GetItem()->Save(0); } else { SetPulse(); } } void MailUi::OnDirty(bool Dirty) { SetCtrlEnabled(IDM_SAVE, Dirty); SetCtrlEnabled(IDM_SAVE_CLOSE, Dirty); if (Dirty) { SetPulse(60 * 1000); // every minute } else { SetPulse(); } } bool MailUi::CallMethod(const char *Name, LVariant *Dst, LArray &Arg) { ScribeDomType Method = StrToDom(Name); *Dst = false; switch (Method) { case SdShowRemoteContent: // Type: () if (HtmlView) { bool Always = Arg.Length() > 0 ? Arg[0]->CastBool() : false; if (Always && GetItem()) { auto From = GetItem()->GetFrom(); if (From) App->RemoteContent_AddSender(From->GetStr(FIELD_EMAIL), true); else LgiTrace("%s:%i - No from address.\n", _FL); } IgnoreShowImgNotify = true; HtmlView->SetLoadImages(true); IgnoreShowImgNotify = false; PostEvent(M_UPDATE); *Dst = true; } break; case SdSetHtml: // Type: (String Html) if (HtmlView) { if (Arg.Length() > 0) { HtmlView->Name(Arg[0]->Str()); *Dst = true; } } break; default: return false; } return true; } LMessage::Result MailUi::OnEvent(LMessage *Msg) { switch (Msg->Msg()) { case M_SET_HTML: { LAutoPtr s((LString*)Msg->A()); if (s && HtmlView) HtmlView->Name(*s); break; } case M_NEEDS_CAP: { LAutoString c((char*)Msg->A()); NeedsCapability(c); return 0; } case M_RESIZE_IMAGE: { LAutoPtr Job((ImageResizeThread::Job*)Msg->A()); if (Job && GetItem()) { // Find the right attachment... List Attachments; if (GetItem()->GetAttachments(&Attachments)) { Attachment *Match = NULL; for (auto a: Attachments) { LString Nm = a->GetName(); if (Nm.Equals(Job->FileName)) { Match = a; break; } } if (Match) { if (Job->Data) Match->Set(Job->Data); auto Mt = Match->GetMimeType(); if (_stricmp(Mt, "image/jpeg")) { auto Name = Match->GetName(); char *Ext = LGetExtension(Name); if (Ext) *Ext = 0; LString NewName = Name; NewName += "jpg"; Match->SetName(NewName); Match->SetMimeType("image/jpeg"); } Match->SetIsResizing(false); LDataI *AttachPoint = GetItem()->GetFileAttachPoint(); if (AttachPoint) { // Do final save to the mail store... Match->GetObject()->Save(AttachPoint); } else { LAssert(0); Match->DecRef(); Match = NULL; } if (CmdAfterResize) { bool Working = IsWorking(); if (!Working) { // All resizing work is done... OnCommand(CmdAfterResize, 0, NULL); return 0; } } } else LAssert(!"No matching attachment image to store resized image in."); } } break; } #if WINNATIVE case WM_COMMAND: { LAssert((NativeInt)Commands.Toolbar != 0xdddddddd); if (Commands.Toolbar && Commands.Toolbar->Handle() == (HWND)Msg->b) { return LWindow::OnEvent(Msg); } break; } #endif } return LWindow::OnEvent(Msg); } void MailUi::OnReceiveFiles(LArray &Files) { List Att; GetItem()->GetAttachments(&Att); for (unsigned i=0; iGetName(), f) == 0) { int Result = LgiMsg(this, LLoadString(IDS_ATTACH_WARNING_DLG), LLoadString(IDS_ATTACHMENTS), MB_YESNO); if (Result == IDNO) { Add = false; break; } } } if (Add) { Attachment *a = GetItem()->AttachFile(this, Path); if (a) { Attachments->Insert(a); Attachments->ResizeColumnsToContent(); } } } } ////////////////////////////////////////////////////////////////////////////// bool Mail::PreviewLines = false; bool Mail::RunMailPipes = true; List Mail::NewMailLst; bool Mail::AdjustDateTz = true; LHashTbl,Mail*> Mail::MessageIdMap; Mail::Mail(ScribeWnd *app, LDataI *object) : Thing(app, object) { DefaultObject(object); d = new MailPrivate(this); _New(); } Mail::~Mail() { if (GetObject()) { auto Id = GetObject()->GetStr(FIELD_MESSAGE_ID); if (Id) MessageIdMap.Delete(Id); } NewMailLst.Delete(this); _Delete(); DeleteObj(d); } void Mail::_New() { SendAttempts = 0; Container = 0; FlagsCache = -1; PreviewCacheX = 0; Cursor = 0; ParentFile = 0; PreviousMail = 0; NewEmail = NewEmailNone; Ui = 0; TotalSizeCache = -1; } void Mail::_Delete() { if (ParentFile) { ParentFile->SetMsg(0); } UnloadAttachments(); PreviewCache.DeleteObjects(); DeleteObj(Container); if (Ui) Ui->PostEvent(M_CLOSE); if (Ui) Ui->SetItem(0); } bool Mail::AppendItems(LSubMenu *Menu, const char *Param, int Base) { auto Remove = Menu->AppendSub(LLoadString(IDS_REMOVE)); if (Remove) { Remove->AppendItem("'>'", IDM_REMOVE_GRTH, true); Remove->AppendItem("'> '", IDM_REMOVE_GRTH_SP, true); Remove->AppendItem(LLoadString(IDS_HTML_TAGS), IDM_REMOVE_HTML, true); } auto FilterMenu = Menu->AppendSub(LLoadString(IDS_FILTER)); if (FilterMenu) { FilterMenu->SetImageList(App->GetIconImgList(), false); Actions.Empty(); AddActions(FilterMenu, Actions, App->GetThingSources(MAGIC_FILTER)); } auto Convert = Menu->AppendSub(LLoadString(IDS_CONVERT_SELECTION)); if (Convert) { Convert->AppendItem("To Base64", IDM_CONVERT_BIN_TO_B64, true); Convert->AppendItem("To Binary", IDM_CONVERT_B64_TO_BIN, true); } if (!TestFlag(GetFlags(), MAIL_CREATED)) { auto Charset = Menu->AppendSub(LLoadString(L_CHANGE_CHARSET)); if (Charset) { int n=0; for (LCharset *c = LGetCsList(); c->Charset; c++, n++) Charset->AppendItem(c->Charset, IDM_CHARSET_BASE + n, c->IsAvailable()); } } return true; } bool Mail::OnMenu(LDocView *View, int Id, void *Context) { const char *RemoveStr = 0; switch (Id) { case IDM_REMOVE_GRTH: { RemoveStr = ">"; break; } case IDM_REMOVE_GRTH_SP: { RemoveStr = "> "; break; } case IDM_REMOVE_HTML: { auto s = View ? View->Name() : 0; if (s) { auto n = DeHtml(s); if (n) { View->Name(n); DeleteArray(n); } } break; } default: { if (Id >= IDM_CHARSET_BASE) { int n=0; LCharset *c; for (c = LGetCsList(); c->Charset; c++, n++) { if (Id - IDM_CHARSET_BASE == n) { break; } } if (c->Charset) { SetBodyCharset((char*)c->Charset); SetDirty(); if (GetBodyCharset() && Ui) { Ui->OnLoad(); } } } else if (Id >= IDM_FILTER_BASE) { Filter *Action = Actions[Id-IDM_FILTER_BASE]; if (Action) { // Save the message... SetDirty(false); // Hide the mail window... if (Ui) Ui->Visible(false); // Do the action bool Stop; Mail *This = this; Action->DoActions(This, Stop); if (This != this) break; if (Ui) DeleteObj(Ui); return true; } } break; } #ifdef _DEBUG case IDM_CONVERT_BIN_TO_B64: { char *t = View->GetSelection(); if (t) { size_t In = strlen(t); size_t Len = BufferLen_BinTo64(In); char *B64 = new char[Len+1]; if (B64) { ConvertBinaryToBase64(B64, Len, (uchar*)t, In); B64[Len] = 0; char Temp[256]; ConvertBase64ToBinary((uchar*)Temp, sizeof(Temp), B64, Len); char16 *Str = Utf8ToWide(B64); if (Str) { LTextView3 *Tv = dynamic_cast(View); if (Tv) { Tv->DeleteSelection(); Tv->Insert(Tv->GetCaret(), Str, Len); } DeleteArray(Str); } DeleteArray(B64); } } break; } case IDM_CONVERT_B64_TO_BIN: { char *t = View->GetSelection(); if (t) { size_t In = strlen(t); size_t Len = BufferLen_64ToBin(In); char *Bin = new char[Len+1]; if (Bin) { ssize_t Out = ConvertBase64ToBinary((uchar*)Bin, Len, t, In); Bin[Out] = 0; char16 *Str = Utf8ToWide(Bin, Out); if (Str) { LTextView3 *Tv = dynamic_cast(View); if (Tv) { Tv->DeleteSelection(); Tv->Insert(Tv->GetCaret(), Str, Out); } DeleteArray(Str); } DeleteArray(Bin); } } break; } #endif } if (RemoveStr) { size_t TokenLen = strlen(RemoveStr); auto s = View ? View->Name() : 0; if (s) { LMemQueue Temp; auto Start = s; const char *End; while (*Start) { // seek to EOL for (End = Start; *End && *End != '\n'; End++); End++; ssize_t Len = End - Start; if (_strnicmp(Start, RemoveStr, TokenLen) == 0) { Temp.Write((uchar*)Start + TokenLen, Len - TokenLen); } else { Temp.Write((uchar*)Start, Len); } Start = End; } int Size = (int)Temp.GetSize(); char *Buf = new char[Size+1]; if (Buf) { Temp.Read((uchar*) Buf, Size); Buf[Size] = 0; View->Name(Buf); DeleteArray(Buf); } } } return true; } LDocumentEnv::LoadType Mail::GetContent(LoadJob *&j) { if (!j) return LoadError; LUri Uri(j->Uri); if ( Uri.sProtocol && ( !_stricmp(Uri.sProtocol, "http") || !_stricmp(Uri.sProtocol, "https") || !_stricmp(Uri.sProtocol, "ftp") ) ) { // We don't check OPT_HtmlLoadImages here because it's done elsewhere: // - ScribeWnd::CreateTextControl calls LHtml::SetLoadImages with the value from OPT_HtmlLoadImages // - LTag::LoadImage checks LHtml::GetLoadImages // // If there is a remote job here, it's because it's probably whitelisted. if (!Worker) Worker = App->GetImageLoader(); if (!Worker) return LoadError; Worker->AddJob(j); j = 0; return LoadDeferred; } else if (Uri.sProtocol && !_stricmp(Uri.sProtocol, "file")) { if (!_strnicmp(Uri.sPath, "//", 2)) { // This seems to hang the windows CreateFile function... } else { // Is it a local file then? if (j->pDC.Reset(GdcD->Load(Uri.sPath))) { return LoadImmediate; } } } else { List Files; if (GetAttachments(&Files)) { auto Dir = FilePart(j->Uri); Attachment *a = NULL; for (auto It = Files.begin(); It != Files.end(); It++) { a = *It; if (_strnicmp(j->Uri, "cid:", 4) == 0) { char *ContentId = j->Uri + 4; auto AttachmentId = a->GetContentId(); if (AttachmentId) { if (AttachmentId[0] == '<') { auto s = AttachmentId + 1; auto e = strrchr(s, '>'); if (e) { ssize_t len = e - s; if (strlen(ContentId) == len && !strncmp(s, ContentId, len)) break; } } else if (!strcmp(AttachmentId, ContentId)) { break; } } } else { auto Name = a->GetName(); if (Name) { auto NameDir = FilePart(Name); if (_stricmp(NameDir, Dir) == 0) { break; } } } } if (a) { j->MimeType = a->GetMimeType(); j->ContentId = a->GetContentId(); if (j->Pref == LoadJob::FmtStream) { j->Filename = a->GetName(); j->Stream = a->GetObject()->GetStream(_FL); return LoadImmediate; } else { char *Tmp = ScribeTempPath(); if (Tmp) { auto File = a->GetName(); auto Ext = LGetExtension(File); char s[MAX_PATH_LEN] = ""; LString part; do { if (part.Printf("%x.%s", LRand(), Ext) < 0) return LoadError; if (!LMakePath(s, sizeof(s), Tmp, part)) return LoadError; } while (LFileExists(s)); if (a->SaveTo(s, true)) { if (j->Pref == LoadJob::FmtFilename) { j->Filename = s; return LoadImmediate; } else { int Promote = GdcD->SetOption(GDC_PROMOTE_ON_LOAD, 0); j->pDC.Reset(GdcD->Load(s)); j->Filename = a->GetName(); GdcD->SetOption(GDC_PROMOTE_ON_LOAD, Promote); FileDev->Delete(s, false); return LoadImmediate; } } } } } } } return LoadError; } class MailCapabilities : public LLayout { LArray MissingCaps; LDocView *Doc; LButton *Install; public: MailCapabilities(LDocView *d) { Doc = d; Install = 0; } const char *GetClass() { return "MailCapabilities"; } void OnPosChange() { LRect c = GetClient(); if (MissingCaps.Length()) { if (!Install) { if ((Install = new LButton(IDOK, 0, 0, -1, -1, "Install"))) Install->Attach(this); } LRect r = c; r.x1 = r.x2 - Install->X(); r.y2 -= 7; Install->SetPos(r); } } void OnPaint(LSurface *pDC) { LRect cli = GetClient(); if (MissingCaps.Length()) { char Msg[256]; int c = sprintf_s(Msg, sizeof(Msg), "This content requires "); for (unsigned i=0; iTransparent(false); LSysFont->Colour(L_TEXT, L_MED); ds.Draw(pDC, cli.x1, cli.y1, &cli); } else { pDC->Colour(L_MED); pDC->Rectangle(); } } }; LDocView *Mail::CreateView( MailViewOwner *Owner, LString MimeType, bool Sunken, size_t MaxBytes, bool NoEdit) { bool Created = TestFlag(GetFlags(), MAIL_CREATED); bool Edit = NoEdit ? false : Created; bool ReadOnly = !Created; LAutoString Mem; LVariant DefAlt; App->GetOptions()->GetValue(OPT_DefaultAlternative, DefAlt); auto TextBody = GetBody(); auto TextCharset = GetBodyCharset(); auto HtmlBody = GetHtml(); auto HtmlCharset = GetHtmlCharset(); const char *CtrlType = NULL; if (!MimeType) { bool TextValid = TextBody != NULL; bool HtmlValid = HtmlBody != NULL; if (TextValid && HtmlValid) MimeType = DefAlt.CastInt32() ? sTextHtml : sTextPlain; else if (TextValid) MimeType = sTextPlain; else if (HtmlValid) MimeType = sTextHtml; else return NULL; } #ifdef WINDOWS if (DefAlt.CastInt32() == 2 && MimeType == sTextHtml) CtrlType = sApplicationInternetExplorer; else #endif CtrlType = MimeType; const char *Content, *Charset; if (MimeType == sTextHtml) { Content = HtmlBody; Charset = HtmlCharset; } else { Content = TextBody; Charset = TextCharset; } // Emoji check LVariant NoEmoji; App->GetOptions()->GetValue(OPT_NoEmoji, NoEmoji); // Check if the control needs changing LDocView *View = Owner->GetDoc(MimeType); if (View) { const char *ViewMimeType = View->GetMimeType(); if (MimeType != ViewMimeType) { Owner->SetDoc(NULL, ViewMimeType); View = NULL; } } if (!View) { View = App->CreateTextControl( MimeType == sTextHtml ? IDC_HTML_VIEW : IDC_TEXT_VIEW, MimeType, Edit, this); } if (View) { // Control setup View->Sunken(Sunken); View->SetReadOnly(ReadOnly); View->SetEnv(this); LVariant UseCid = true; View->SetValue(LDomPropToString(HtmlImagesLinkCid), UseCid); LVariant LoadImages; App->GetOptions()->GetValue(OPT_HtmlLoadImages, LoadImages); bool AppLoadImages = LoadImages.CastInt32() != 0; bool MailLoadImages = TestFlag(GetFlags(), MAIL_SHOW_IMAGES); const char *SenderAddr = GetFrom() ? GetFrom()->GetStr(FIELD_EMAIL) : NULL; auto SenderStatus = App->RemoteContent_GetSenderStatus(SenderAddr); View->SetLoadImages ( SenderStatus != RemoteNeverLoad && ( AppLoadImages || MailLoadImages || SenderStatus == RemoteAlwaysLoad ) ); // Attach control Owner->SetDoc(View, MimeType); LCharset *CsInfo = LGetCsInfo(Charset); // Check for render scripts LArray Renderers; LString RenderMsg = "Rendering..."; if (App->GetScriptCallbacks(LRenderMail, Renderers)) { for (auto r: Renderers) { LVirtualMachine Vm; LScriptArguments Args(&Vm); Args.New() = new LVariant(App); Args.New() = new LVariant(this); Args.New() = new LVariant((void*)NULL); bool Status = App->ExecuteScriptCallback(*r, Args); Args.DeleteObjects(); if (Status) { auto Ret = Args.GetReturn(); if (Ret->IsString() || Ret->CastInt32()) { if (Ret->IsString()) RenderMsg = Ret->Str(); d->Renderer.Reset(new MailRendererScript(this, r)); break; } } } } if (d->Renderer) { LString Nm; if (MimeType.Equals(sTextHtml)) Nm.Printf("%s", RenderMsg.Get()); else Nm = RenderMsg; View->Name(Nm); } else { // Send the data to the control size_t ContentLen = Content ? strlen(Content) : 0; Html1::LHtml *Html = dynamic_cast(View); if (MimeType.Equals(sTextHtml)) { if (CsInfo) { int OverideDocCharset = *Charset == '>' ? 1 : 0; View->SetCharset(Charset + OverideDocCharset); if (Html) Html->SetOverideDocCharset(OverideDocCharset != 0); } else { View->SetCharset(0); if (Html) Html->SetOverideDocCharset(0); } View->Name(Content); } else { LAutoPtr Utf32((uint32_t*)LNewConvertCp("utf-32", Content, Charset ? Charset : (char*)"utf-8", MaxBytes > 0 ? MIN(ContentLen, MaxBytes) : ContentLen)); if (Utf32) { int Len = 0; while (Utf32[Len]) Len++; #if 0 LFile f; if (f.Open("c:\\temp\\utf32.txt", O_WRITE)) { uchar bom[4] = { 0xff, 0xfe, 0, 0 }; f.Write(bom, 4); f.Write(Utf32, Len * sizeof(uint32)); f.Close(); } #endif } LAutoWString Wide; Wide.Reset((char16*)LNewConvertCp(LGI_WideCharset, Content, Charset ? Charset : (char*)"utf-8", MaxBytes > 0 ? MIN(ContentLen, MaxBytes) : ContentLen)); if (Wide) { View->NameW(Wide); } else { // Fallback... try and show something at least LAutoString t(NewStr(Content, MaxBytes > 0 ? MIN(ContentLen, MaxBytes) : ContentLen)); if (t) { uint8_t *i = (uint8_t*)t.Get(); while (*i) { if (*i & 0x80) *i &= 0x7f; i++; } View->Name(t); } else View->NameW(0); } View->SetFixedWidthFont(TestFlag(GetFlags(), MAIL_FIXED_WIDTH_FONT)); } } } return View; } bool Mail::OnNavigate(LDocView *Parent, const char *Uri) { if (Uri) { if ( _strnicmp(Uri, "mailto:", 7) == 0 || ( strchr(Uri, '@') && !strchr(Uri, '/') ) ) { // Mail address return App->CreateMail(0, Uri, 0) != 0; } else { return LDefaultDocumentEnv::OnNavigate(Parent, Uri); } } return false; } void Mail::Update() { TotalSizeCache = -1; LListItem::Update(); } LString::Array ParseIdList(const char *In) { LString::Array result; if (!In) return result; while (*In && strchr(WhiteSpace, *In)) In++; if (*In == '<') { // Standard msg-id list.. for (auto s = In; s && *s; ) { s = strchr(s, '<'); if (!s) break; while (*s == '<') s++; char *e = strchr(s, '>'); if (e) { result.New().Set(s, e-s); s = e + 1; } else break; } } else { // Non compliant msg-id list... const char Delim[] = ", \t\r\n"; for (auto s = In; s && *s; ) { if (strchr(Delim, *s)) s++; else { auto Start = s; while (*s && !strchr(Delim, *s)) s++; result.New().Set(Start, s - Start); } } } return result; } void Base36(char *Out, uint64 In) { while (In) { int p = (int)(In % 36); if (p < 10) { *Out++ = '0' + p; } else { *Out++ = 'A' + p - 10; } In /= 36; } *Out++ = 0; } void Mail::ClearCachedItems() { TotalSizeCache = -1; PreviewCacheX = -1; PreviewCache.DeleteObjects(); Attachment *a; int i = 0; while ((a = Attachments[i])) { if (!a->DecRef()) i++; } } void Mail::NewRecipient(char *Email, char *Name) { LDataPropI *a = GetTo()->Create(GetObject()->GetStore()); if (a) { a->SetStr(FIELD_EMAIL, Email); a->SetStr(FIELD_NAME, Name); GetTo()->Insert(a); } } void Mail::PrepSend() { // Set flags and other data SetDirty(); int OldFlags = GetFlags(); SetFlags((OldFlags | MAIL_READY_TO_SEND) & ~MAIL_SENT); // we want to send now... // Check we're in the Outbox ScribeFolder *OutBox = App->GetFolder(FOLDER_OUTBOX, GetObject()); if (OutBox) { ScribeFolder *f = GetFolder(); if (!f || f != OutBox) { LArray Items; Items.Add(this); OutBox->MoveTo(Items); } } } bool Mail::Send(bool Now) { // Check for any "on before send" callbacks: bool AllowSend = true; LArray Callbacks; if (App->GetScriptCallbacks(LMailOnBeforeSend, Callbacks)) { for (unsigned i=0; AllowSend && iExecuteScriptCallback(c, Args)) { if (!Args.GetReturn()->CastInt32()) AllowSend = false; } Args.DeleteObjects(); } } } if (!AllowSend) return false; // Set the ready to send flag.. bool IsInPublicFolder = GetFolder() && GetFolder()->IsPublicFolders(); if (!IsInPublicFolder) { PrepSend(); if (Now) { // Kick off send thread if relevant LVariant Offline; App->GetOptions()->GetValue(OPT_WorkOffline, Offline); if (!Offline.CastInt32() && !IsInPublicFolder) { App->PostEvent(M_COMMAND, IDM_SEND_MAIL, (LMessage::Param)Handle()); } } } return true; } bool Mail::MailMessageIdMap(bool Add) { if (Add) { auto LoadState = GetLoaded(); if (LoadState < Store3Headers) { LAssert(!"Not loaded yet."); LStackTrace("MailMessageIdMap msg not loaded yet: %i\n", LoadState); return false; } // char *ObjId = GetObject()->GetStr(FIELD_MESSAGE_ID); auto Id = GetMessageId(true); if (!Id) { LAssert(!"No message ID? Impossible!"); GetMessageId(true); return false; } #if 0 LgiTrace("MailMessageIdMap(%i) Old=%s Id=%s Mail=%p\n", Add, ObjId, Id, this); #endif return MessageIdMap.Add(Id, this); } else { auto Id = GetMessageId(); #if 0 LgiTrace("MailMessageIdMap(%i) Id=%s Mail=%p\n", Add, Id, this); #endif return MessageIdMap.Delete(Id); } } Mail *Mail::GetMailFromId(const char *Id) { Mail *m = MessageIdMap.Find(Id); // LgiTrace("GetMailFromId(%s)=%p\n", Id, m); return m; } bool Mail::SetMessageId(const char *MsgId) { if (LAppInst->InThread()) { LAssert(GetObject() != NULL); auto OldId = GetObject()->GetStr(FIELD_MESSAGE_ID); if (OldId) MessageIdMap.Delete(OldId); LAutoString m(NewStr(MsgId)); LAutoString s(TrimStr(m, "<>")); if (GetObject()->SetStr(FIELD_MESSAGE_ID, s)) SetDirty(); return s != 0; } else { // No no no NO NON NOT NADA. LAssert(0); } return false; } LAutoString Mail::GetThreadIndex(int TruncateChars) { LAutoString Id; LAutoString Raw(InetGetHeaderField(GetInternetHeader(), "Thread-Index")); if (Raw) { Id = ConvertThreadIndex(Raw, TruncateChars); } return Id; } const char *Mail::GetMessageId(bool Create) { LAssert(GetObject() != NULL); d->MsgIdCache = GetObject()->GetStr(FIELD_MESSAGE_ID); if (!d->MsgIdCache) { bool InThread = GetCurrentThreadId() == LAppInst->GetGuiThreadId(); LAutoString Header(InetGetHeaderField(GetInternetHeader(), "Message-ID")); if (Header) { LAssert(InThread); if (InThread) { auto Ids = ParseIdList(Header); SetMessageId(d->MsgIdCache = Ids[0]); } } if (!d->MsgIdCache && Create) { LAssert(InThread); if (InThread) { auto FromEmail = GetFromStr(FIELD_EMAIL); const char *At = FromEmail ? strchr(FromEmail, '@') : 0; LVariant Email; if (!At) { if (App->GetOptions()->GetValue(OPT_Email, Email) && Email.Str()) { At = strchr(Email.Str(), '@'); } else { At = "@domain.com"; } } if (At) { char m[96], a[32], b[32]; Base36(a, LCurrentTime()); Base36(b, LRand(RAND_MAX)); sprintf_s(m, sizeof(m), "<%s.%i%s%s>", a, LRand(RAND_MAX), b, At); if (GetObject()->SetStr(FIELD_MESSAGE_ID, m)) SetDirty(); d->MsgIdCache = GetObject()->GetStr(FIELD_MESSAGE_ID); } else LgiTrace("%s:%i - Error, no '@' in %s.\n", _FL, FromEmail); } else LgiTrace("%s:%i - Error, not in thread.\n", _FL); } } else { d->MsgIdCache = d->MsgIdCache.Strip("<>").Strip(); } LAssert ( (!d->MsgIdCache && !Create) || (d->MsgIdCache && !strchr(d->MsgIdCache, '\n')) ); return d->MsgIdCache; } bool Mail::GetReferences(LString::Array &Ids) { LAutoString References(InetGetHeaderField(GetInternetHeader(), "References")); if (References) { Ids = ParseIdList(References); } LAutoString InReplyTo(InetGetHeaderField(GetInternetHeader(), "In-Reply-To")); if (InReplyTo) { auto To = ParseIdList(InReplyTo); bool Has = false; auto &r = To[0]; if (r) { for (auto h: Ids) { if (!strcmp(h, r)) Has = true; } if (!Has) { To.Delete(r); Ids.New() = r; } } To.DeleteArrays(); } if (Ids.Length() == 0) { LAutoString Id = GetThreadIndex(5); if (Id) { size_t Len = strlen(Id); size_t Bytes = Len >> 1; if (Bytes >= 22) { Ids.New() = Id.Get(); } } } return Ids.Length() > 0; } LString AddrToHtml(LDataPropI *a) { LString s; if (a) { auto Name = a->GetStr(FIELD_NAME); auto Addr = a->GetStr(FIELD_EMAIL); LXmlTree t; LAutoString eName(t.EncodeEntities(Name)); LAutoString eAddr(t.EncodeEntities(Addr)); if (Name && Addr) s.Printf("%s", eAddr.Get(), eName.Get()); else if (Name) s = eName.Get(); else if (Addr) s.Printf("%s", eAddr.Get(), eAddr.Get()); } return s; } LDataPropI *FindMimeSeg(LDataPropI *s, char *Type) { if (!s) return 0; const char *Mt = s->GetStr(FIELD_MIME_TYPE); if (!Mt) Mt = "text/plain"; if (!_stricmp(Type, Mt)) return s; LDataIt c = s->GetList(FIELD_MIME_SEG); if (!c) return 0; for (LDataPropI *i=c->First(); i; i=c->Next()) { LDataPropI *Child = FindMimeSeg(i, Type); if (Child) return Child; } return 0; } bool Mail::GetAttachmentObjs(LArray &Objs) { if (!GetObject()) return false; CollectAttachments(&Objs, NULL, NULL, NULL, GetObject()->GetObj(FIELD_MIME_SEG)); return Objs.Length() > 0; } void DescribeMime(LStream &p, LDataI *Seg) { auto Mt = Seg->GetStr(FIELD_MIME_TYPE); p.Print("%s", Mt); auto Cs = Seg->GetStr(FIELD_CHARSET); if (Cs) p.Print(" - %s", Cs); p.Print("
\n"); LDataIt c = Seg->GetList(FIELD_MIME_SEG); if (c && c->First()) { p.Print("
\n"); for (LDataPropI *i=c->First(); i; i=c->Next()) { LDataI *a = dynamic_cast(i); if (a) DescribeMime(p, a); } p.Print("
\n"); } } bool Mail::GetVariant(const char *Name, LVariant &Value, const char *Array) { ScribeDomType Fld = StrToDom(Name); switch (Fld) { case SdFrom: // Type: ListAddr { Value = GetFrom(); break; } case SdFromHtml: // Type: String { LString s = AddrToHtml(GetFrom()); if (s.Length() == 0) return false; Value = s; break; } case SdContact: // Type: Contact { if (!GetFrom()) return false; auto Addr = GetFrom()->GetStr(FIELD_EMAIL); if (!Addr) return false; Contact *c = Contact::LookupEmail(Addr); if (!c) return App->GetVariant("NoContact", Value); Value = (LDom*)c; break; } case SdTo: // Type: ListAddr[] { if (Array) { LDataIt To = GetTo(); int Idx = atoi(Array); bool Create = false; if (Idx < 0) { Create = true; Idx = -Idx; } LDataPropI *a = Idx < (int)To->Length() ? (*To)[Idx] : 0; if (!a && Create) { if ((a = To->Create(GetObject()->GetStore()))) { To->Insert(a); } } Value = a; } else if (Value.SetList()) { LDataIt To = GetTo(); for (LDataPropI *a=To->First(); a; a=To->Next()) { LVariant *Recip = new LVariant; if (Recip) { *Recip = a; Value.Value.Lst->Insert(Recip); } } } else return false; break; } case SdToHtml: // Type: String { if (GetTo()) { LStringPipe p; for (LDataPropI *t=GetTo()->First(); t; t=GetTo()->Next()) { if (p.GetSize()) p.Write((char*)", ", 2); LString s = AddrToHtml(t); if (s) p.Write(s, s.Length()); } Value.Type = GV_STRING; Value.Value.String = p.NewStr(); } break; } case SdSubject: // Type: String { Value = GetSubject(); break; } case SdBody: // Type: String { if (Array) { auto Body = GetBody(); LAutoString h; for (auto c = Body; c && *c; ) { while (*c && strchr(WhiteSpace, *c)) c++; auto Start = c; while (*c && *c != ':' && *c != '\n') c++; if (*c == ':') { if (Start[0] == '\"' && c[1] == '\"') c++; LString s(Start, c - Start); s = s.Strip("\'\" \t[]<>{}:"); if (s.Equals(Array)) { // Match... c++; while (*c && strchr(WhiteSpace, *c)) c++; Start = c; while (*c && *c != '\n') c++; while (c > Start && strchr(WhiteSpace, c[-1])) c--; h.Reset(NewStr(Start, c - Start)); break; } else { while (*c && *c != '\n') c++; } } if (*c == '\n') c++; } if (h) { if (GetBodyCharset()) { char *u = (char*)LNewConvertCp("utf-8", h, GetBodyCharset()); if (u) { Value.OwnStr(u); } else { Value.OwnStr(h.Release()); } } else { Value.OwnStr(h.Release()); } } } else { Value = GetBody(); } break; } case SdBodyAsText: // Type: String { size_t MaxSize = ValidStr(Array) ? atoi(Array) : -1; if (ValidStr(GetBody()) && ValidStr(GetHtml())) { LVariant Def; App->GetOptions()->GetValue(OPT_DefaultAlternative, Def); if (Def.CastInt32()) { goto DoHtml; } goto DoText; } else if (ValidStr(GetBody())) { DoText: LAutoString CharSet = GetCharSet(); auto Txt = GetBody(); if (CharSet) { size_t TxtLen = strlen(Txt); Value.OwnStr((char*)LNewConvertCp("utf-8", Txt, CharSet, MIN(TxtLen, MaxSize))); } else Value = Txt; } else if (ValidStr(GetHtml())) { DoHtml: auto v = HtmlToText(GetHtml(), GetHtmlCharset()); Value = v.Get(); } else return false; break; } case SdBodyAsHtml: // Type: String { // int MaxSize = ValidStr(Array) ? atoi(Array) : -1; MailPrivate::HtmlBody *b = d->GetBody(); if (!b) return false; LStringPipe p; p.Print("
\n%s\n
\n", ScribeReplyClass, b->Html.Get()); Value.OwnStr(p.NewStr()); LgiTrace("Value=%s\n", Value.Str()); break; } case SdHtmlHeadFields: // Type: String { MailPrivate::HtmlBody *b = d->GetBody(); if (!b) return false; LStringPipe p; if (ValidStr(b->Charset)) p.Print("\t\n", b->Charset.Get()); p.Print("\t"); Value.OwnStr(p.NewStr()); break; } case SdMessageID: // Type: String { Value = GetMessageId(); return true; } case SdInternetHeaders: // Type: String { Value = GetInternetHeader(); break; } case SdInternetHeader: // Type: String[] { if (!Array || !GetInternetHeader()) return false; LAutoString s(InetGetHeaderField(GetInternetHeader(), Array)); if (s) Value = s; else Value.Empty(); break; } case SdPriority: // Type: Int32 { Value = (int)GetPriority(); break; } case SdHtml: // Type: String { Value = GetHtml(); break; } case SdFlags: // Type: Int32 { if (Array) { int Flag = StringToMailFlag(Array); Value = (GetFlags() & Flag) != 0; } else { Value = (int)GetFlags(); } break; } case SdFolder: // Type: ScribeFolder { ScribeFolder *f = GetFolder(); if (!f) return false; Value = (LDom*)f; break; } case SdScribe: // Type: ScribeWnd { Value = (LDom*)App; break; } case SdMail: // Type: Mail { Value = PreviousMail; break; } case SdDateSent: // Type: DateTime { Value = GetDateSent(); break; } case SdDateReceived: // Type: DateTime { Value = GetDateReceived(); break; } case SdSig: // Type: String { bool Type = false; if (Array && stristr(Array, "html")) Type = true; Value = GetSig(Type); break; } case SdSize: // Type: Int64 { Value = TotalSizeof(); break; } case SdLabel: // Type: String { Value = GetLabel(); break; } case SdAttachments: // Type: Int32 { LArray Lst; GetAttachmentObjs(Lst); Value = (int)Lst.Length(); break; } case SdAttachment: // Type: Attachment[] { List Files; if (GetAttachments(&Files) && Array) { int i = atoi(Array); Value = Files[i]; if (Value.Type == GV_DOM) return true; } LAssert(!"Not a valid attachment?"); return false; } case SdMimeTree: // Type: String { LDataI *Root = dynamic_cast(GetObject()->GetObj(FIELD_MIME_SEG)); if (!Root) return false; LStringPipe p(256); DescribeMime(p, Root); Value.OwnStr(p.NewStr()); break; } case SdUi: // Type: LView { Value = Ui; break; } case SdType: // Type: Int32 { Value = GetObject()->Type(); break; } case SdSelected: // Type: Bool { Value = Select(); break; } case SdRead: // Type: Bool { Value = (GetFlags() & MAIL_READ) != 0; break; } case SdShowImages: // Type: Bool { Value = (GetFlags() & MAIL_SHOW_IMAGES) != 0; break; } case SdColour: // Type: Int32 { Value = GetMarkColour(); break; } case SdReceivedDomain: // Type: String { Value = GetFieldText(FIELD_RECEIVED_DOMAIN); break; } default: { return false; } } return true; } #define DomSetStr(Dom, Fld) \ case Dom: \ if (GetObject() && Value.Type == GV_STRING) \ GetObject()->SetStr(Fld, Value.Str()); \ else \ LAssert(!"Missing object or type err"); \ break; #define DomSetInt(Dom, Fld) \ case Dom: \ if (GetObject() && Value.Type == GV_INT32) \ GetObject()->SetInt(Fld, Value.Value.Int); \ else \ LAssert(!"Missing object or type err"); \ break; #define DomSetDate(Dom, Fld) \ case Dom: \ if (GetObject() && Value.Type == GV_DATETIME) \ GetObject()->SetDate(Fld, Value.Value.Date); \ else \ LAssert(!"Missing object or type err"); \ break; bool Mail::SetVariant(const char *Name, LVariant &Value, const char *Array) { ScribeDomType Fld = StrToDom(Name); switch (Fld) { DomSetStr(SdSubject, FIELD_SUBJECT) DomSetStr(SdMessageID, FIELD_MESSAGE_ID) DomSetStr(SdInternetHeaders, FIELD_INTERNET_HEADER) DomSetInt(SdPriority, FIELD_PRIORITY) DomSetDate(SdDateSent, FIELD_DATE_SENT) DomSetDate(SdDateReceived, FIELD_DATE_RECEIVED) case SdBody: { if (!SetBody(Value.Str())) return false; break; } case SdHtml: { if (!SetHtml(Value.Str())) return false; break; } case SdRead: { if (Value.CastInt32()) SetFlags(GetFlags() | MAIL_READ); else SetFlags(GetFlags() & ~MAIL_READ); break; } case SdColour: { auto u32 = Value.IsNull() ? 0 : (uint32_t)Value.CastInt32(); if (!SetMarkColour(u32)) return false; break; } case SdShowImages: { if (Value.CastInt32()) SetFlags(GetFlags() | MAIL_SHOW_IMAGES); else SetFlags(GetFlags() & ~MAIL_SHOW_IMAGES); break; } case SdSelected: { Select(Value.CastInt32() != 0); break; } case SdLabel: { if (!SetLabel(Value.Str())) return false; break; } case SdFlags: { if (Array) { int Flag = StringToMailFlag(Array); if (Value.CastInt32()) // Set SetFlags(GetFlags() | Flag); else SetFlags(GetFlags() & ~Flag); } else { SetFlags(Value.CastInt32()); } break; } default: { return false; } } SetDirty(); Update(); return true; } bool Mail::CallMethod(const char *MethodName, LVariant *ReturnValue, LArray &Args) { ScribeDomType Fld = StrToDom(MethodName); switch (Fld) { default: break; case SdSend: // Type: ([Bool SendNow = true]) { bool Now = Args.Length() > 0 ? Args[0]->CastInt32() != 0 : true; bool Result = Send(Now); if (ReturnValue) *ReturnValue = Result; return true; } case SdAddCalendarEvent: // Type: ([Bool AddPopupReminder]) { bool AddPopupReminder = Args.Length() > 0 ? Args[0]->CastInt32() != 0 : true; bool Result = AddCalendarEvent(NULL, AddPopupReminder, NULL); if (ReturnValue) *ReturnValue = Result; return true; } case SdGetRead: // Type: () { *ReturnValue = (GetFlags() & MAIL_READ) != 0; return true; } case SdSetRead: // Type: (Bool IsRead = true) { bool Rd = Args.Length() ? Args[0]->CastInt32() != 0 : true; auto Flags = GetFlags(); if (Rd) SetFlags(Flags | MAIL_READ); else SetFlags(Flags & ~MAIL_READ); *ReturnValue = true; return true; } case SdBayesianChange: // Type: (int SpamWordOffset, int HamWordOffset) { if (Args.Length() != 2) { LgiTrace("%s:%i - Invalid arg count, expecting (int SpamWordOffset, int HamWordOffset)\n", _FL); *ReturnValue = false; return true; } auto SpamWordOffset = Args[0]->CastInt32(); auto HamWordOffset = Args[1]->CastInt32(); if (SpamWordOffset > 1) App->OnBayesianMailEvent(this, BayesMailUnknown, BayesMailSpam); else if (SpamWordOffset < 0) App->OnBayesianMailEvent(this, BayesMailSpam, BayesMailUnknown); if (HamWordOffset > 1) App->OnBayesianMailEvent(this, BayesMailUnknown, BayesMailHam); else if (HamWordOffset < 1) App->OnBayesianMailEvent(this, BayesMailHam, BayesMailUnknown); break; } case SdBayesianScore: // Type: () { double Result; if (App->IsSpam(Result, this)) { *ReturnValue = Result; return true; } break; } case SdSearchHtml: // Type: (String SearchExpression) { if (Args.Length() != 2) { LgiTrace("%s:%i - Method needs 1 argument.\n", _FL); *ReturnValue = false; return true; } auto Html = GetHtml(); if (!Html) { LgiTrace("%s:%i - No HTML to parse.\n", _FL); *ReturnValue = false; return true; } // auto Cs = GetHtmlCharset(); SearchHtml(ReturnValue, Html, Args[0]->Str(), Args[1]->Str()); return true; } case SdDeleteAsSpam: // Type: () { DeleteAsSpam(App); return true; } } return Thing::CallMethod(MethodName, ReturnValue, Args); } char *Mail::GetDropFileName() { if (!DropFileName) { LString Subj = GetSubject(); Subj = Subj.Strip(" \t\r\n."); DropFileName.Reset(MakeFileName(ValidStr(Subj) ? Subj.Get() : (char*)"Untitled", "eml")); } return DropFileName; } bool Mail::GetDropFiles(LString::Array &Files) { if (!GetDropFileName()) return false; if (!LFileExists(DropFileName)) { LAutoPtr F(new LFile); if (F->Open(DropFileName, O_WRITE)) { F->SetSize(0); if (!Export(AutoCast(F), sMimeMessage)) return false; } else return false; } if (!LFileExists(DropFileName)) return false; Files.Add(DropFileName.Get()); return true; } OsView Mail::Handle() { return #if LGI_VIEW_HANDLE (Ui) ? Ui->Handle() : #endif NULL; } Thing &Mail::operator =(Thing &t) { Mail *m = t.IsMail(); if (m) { #define CopyStr(dst, src) \ { DeleteArray(dst); dst = NewStr(src); } /* for (LDataPropI *a = m->GetTo()->First(); a; a = m->GetTo()->Next()) { LDataPropI *NewA = GetTo()->Create(Object->GetStore()); if (NewA) { *NewA = *a; GetTo()->Insert(NewA); } } */ if (GetObject() && m->GetObject()) { GetObject()->CopyProps(*m->GetObject()); } else LAssert(!"This shouldn't happen right?"); Update(); } return *this; } bool Mail::HasAlternateHtml(Attachment **Attach) { // New system of storing alternate HTML in a Mail object field. if (GetHtml()) return true; // Old way of storing alternate HTML, in an attachment. // pre v1.53 List Files; if (GetAttachments(&Files)) { for (auto a: Files) { if ((_stricmp(a->GetText(0), "attachment.html") == 0 || _stricmp(a->GetText(0), "index.html") == 0) && (a->GetMimeType() ? _stricmp(a->GetMimeType(), "text/html") == 0 : 1)) { if (Attach) { *Attach = a; } return true; } } } // None found return false; } char *Mail::GetAlternateHtml(List *Refs) { char *Status = 0; Attachment *Attach = 0; if (HasAlternateHtml(&Attach)) { if (GetHtml()) { // New system of storing alternate HTML in a Mail object field. Status = NewStr(GetHtml()); } else if (Attach) { // Old way of storing alternate HTML, in an attachment. // pre v1.53 char *Ptr; ssize_t Size; if (Attach->Get(&Ptr, &Size)) { Status = NewStr((char*)Ptr, Size); } } if (Status && Refs) { // Turn all the cid: references into file names... LStringPipe Out; char *n; for (char *s=Status; *s; s=n) { n = stristr(s, "\"cid:"); if (n) { // Extract cid n++; Out.Push(s, n-s); s = n += 4; while (*n && !strchr("\"\' >", *n)) n++; char *Cid = NewStr(s, n-s); if (Cid) { // Find attachment List Files; if (GetAttachments(&Files)) { for (auto a: Files) { if (a->GetContentId() && strcmp(a->GetContentId(), Cid) == 0) { Refs->Insert(a); Out.Push(a->GetName()); } } } } } else { Out.Push(s); break; } } DeleteArray(Status); Status = Out.NewStr(); } } return Status; } bool Mail::WriteAlternateHtml(char *DstFile, int DstFileLen) { bool Status = false; char *Tmp = ScribeTempPath(); List Refs; char *Html; if (Tmp && (Html = GetAlternateHtml(&Refs))) { char FileName[256]; LMakePath(FileName, sizeof(FileName), Tmp, "Alt.html"); if (DstFile) { strcpy_s(DstFile, DstFileLen, FileName); } LFile f; if (f.Open(FileName, O_WRITE)) { size_t Len = strlen(Html); Status = f.Write(Html, Len) == Len; f.Close(); } DeleteArray(Html); for (auto a: Refs) { LMakePath(FileName, sizeof(FileName), Tmp, a->GetName()); FileDev->Delete(FileName, false); a->SaveTo(FileName); } } return Status; } bool Mail::DeleteAttachment(Attachment *File) { bool Status = false; if (File) { if (Attachments.HasItem(File)) { Attachments.Delete(File); if (File->GetObject()) { auto r = File->GetObject()->Delete(); if (r > Store3Error) { auto o = File->GetObject(); if (o->IsOrphan()) o = NULL; // SetObject will delete... File->SetObject(NULL, false, _FL); DeleteObj(o); } } File->DecRef(); File = NULL; if (Attachments.Length() == 0) { // Remove attachments flag SetFlags(GetFlags() & (~MAIL_ATTACHMENTS)); } Update(); if (Ui) { Ui->OnAttachmentsChange(); } } } return Status; } bool Mail::UnloadAttachments() { for (auto it = Attachments.begin(); it != Attachments.end(); ) { Attachment *a = *it; if (!a->DecRef()) it++; } return true; } LArray Mail::GetAttachments() { LArray result; LArray Lst; if (GetAttachmentObjs(Lst)) { LHashTbl, bool> Loaded; for (size_t i=0; iGetObject(), true); } // Load attachments for (unsigned i=0; iSetOwner(this); Attachments.Insert(k); } } } } for (auto a: Attachments) { if (a->GetObject()->UserData != a) LAssert(!"Wut?"); else result.Add(a); } return result; } bool Mail::GetAttachments(List *Files) { if (!Files) return false; auto files = GetAttachments(); for (auto a: files) Files->Add(a); return true; } int64 Mail::TotalSizeof() { if (TotalSizeCache < 0) { int Size = GetObject() ? (int)GetObject()->GetInt(FIELD_SIZE) : -1; if (Size >= 0) { TotalSizeCache = Size; } else { TotalSizeCache = ((GetBody()) ? strlen(GetBody()) : 0) + ((GetHtml()) ? strlen(GetHtml()) : 0); List Attachments; if (GetAttachments(&Attachments)) { for (auto a: Attachments) { TotalSizeCache += a->GetSize(); } } } } return TotalSizeCache; } bool Mail::_GetListItems(List &l, bool All) { LList *ParentList = LListItem::Parent; l.Empty(); if (All) { if (ParentList) { ParentList->GetAll(l); } else { l.Insert(this); } } else { if (ParentList) { ParentList->GetSelection(l); } else if (Select()) { l.Insert(this); } } return l.Length() > 0; } void Mail::GetThread(List &Thread) { MContainer *c; for (c=Container; c->Parent; c=c->Parent); List Stack; Stack.Insert(c); while ((c=Stack[0])) { Stack.Delete(c); for (unsigned i=0; iChildren.Length(); i++) Stack.Insert(c->Children[i]); if (c->Message && !Thread.HasItem(c->Message)) { Thread.Insert(c->Message); } } } void Mail::SetListRead(bool Read) { List Sel; if (!_GetListItems(Sel, false)) return; LArray a; for (auto t: Sel) { auto m = dynamic_cast(t); if (!m) continue; bool isRead = (m->GetFlags() & MAIL_READ) != 0; if (Read ^ isRead) a.Add(m->GetObject()); } if (!a.Length()) return; auto Store = a[0]->GetStore(); if (!Store) { LAssert(0); return; } LVariant read = MAIL_READ; Store->Change(a, FIELD_FLAGS, read, Read ? OpPlusEquals : OpMinusEquals); } void SetFolderCallback(LInput *Dlg, LViewI *EditCtrl, void *Param) { ScribeWnd *App = (ScribeWnd*) Param; auto Str = EditCtrl->Name(); auto Select = new FolderDlg(Dlg, App, MAGIC_MAIL, 0, Str); Select->DoModal([&](auto dlg, auto id) { if (id) EditCtrl->Name(Select->Get()); delete dlg; }); } void Mail::DoContextMenu(LMouse &m, LView *p) { #ifdef _DEBUG LAutoPtr Prof(new LProfile("Mail::DoContextMenu")); Prof->HideResultsIfBelow(100); #endif if (!p) p = Parent; // open the right click menu MarkedState MarkState = MS_None; // Pre-processing for marks List Sel; if (_GetListItems(Sel, false)) { int Marked = 0; for (auto t: Sel) { Mail *m = dynamic_cast(t); if (m) { if (m->GetMarkColour()) { Marked++; } } } if (Marked == 1) { MarkState = MS_One; } else if (Marked) { MarkState = MS_Multiple; } } #ifdef _DEBUG Prof->Add("CreateMenu"); #endif // Create menu LScriptUi s(new LSubMenu); if (s.Sub) { /* Keyboard shortcuts: o - Open d - Delete x - Export e - Set read u - Set unread r - Reply a - Reply all f - Forward b - Bounce m - Mark s - Select marked c - Create filter i - Inspect p - Properties */ LMenuItem *i; s.Sub->SetImageList(App->GetIconImgList(), false); s.Sub->AppendItem(LLoadString(IDS_OPEN), IDM_OPEN, true); i = s.Sub->AppendItem(LLoadString(IDS_DELETE), IDM_DELETE, true); i->Icon(ICON_TRASH); s.Sub->AppendItem(LLoadString(IDS_EXPORT), IDM_EXPORT, true); int AttachBaseMsg = 10000; #ifdef _DEBUG Prof->Add("GetAttachments"); #endif List AttachLst; if (GetAttachments(&AttachLst) && AttachLst[0]) { LSubMenu *Attachments = s.Sub->AppendSub(LLoadString(IDS_SAVE_ATTACHMENT_AS)); if (Attachments) { int n=0; for (auto a: AttachLst) { auto Name = a->GetName(); Attachments->AppendItem(Name?Name:(char*)"Attachment.txt", AttachBaseMsg+(n++), true); } } } s.Sub->AppendSeparator(); #ifdef _DEBUG Prof->Add("Read/Unread"); #endif i = s.Sub->AppendItem(AddAmp(LLoadString(IDS_SET_READ), 'e'), IDM_SET_READ, true); i->Icon(ICON_READ_MAIL); i = s.Sub->AppendItem(AddAmp(LLoadString(IDS_SET_UNREAD), 'u'), IDM_SET_UNREAD, true); i->Icon(ICON_UNREAD_MAIL); s.Sub->AppendSeparator(); #ifdef _DEBUG Prof->Add("Reply/Bounce"); #endif i = s.Sub->AppendItem(AddAmp(LLoadString(IDS_REPLY), 'r'), IDM_REPLY, true); i->Icon(ICON_FLAGS_REPLY); s.Sub->AppendItem(AddAmp(LLoadString(IDS_REPLYALL), 'a'), IDM_REPLY_ALL, true); i = s.Sub->AppendItem(AddAmp(LLoadString(IDS_FORWARD), 'f'), IDM_FORWARD, true); i->Icon(ICON_FLAGS_FORWARD); i = s.Sub->AppendItem(AddAmp(LLoadString(IDS_BOUNCE), 'b'), IDM_BOUNCE, true); i->Icon(ICON_FLAGS_BOUNCE); s.Sub->AppendSeparator(); #ifdef _DEBUG Prof->Add("Thread"); #endif if (App->GetCtrlValue(IDM_THREAD)) { auto Thread = s.Sub->AppendSub(LLoadString(IDS_THREAD)); if (Thread) { Thread->AppendItem(LLoadString(IDS_THREAD_SELECT), IDM_SELECT_THREAD, true); Thread->AppendItem(LLoadString(IDS_THREAD_DELETE), IDM_DELETE_THREAD, true); Thread->AppendItem(LLoadString(IDS_THREAD_IGNORE), IDM_IGNORE_THREAD, true); s.Sub->AppendSeparator(); } } #ifdef _DEBUG Prof->Add("Mark"); #endif auto MarkMenu = s.Sub->AppendSub(AddAmp(LLoadString(IDS_MARK), 'm')); if (MarkMenu) { BuildMarkMenu(MarkMenu, MarkState, (uint32_t)GetMarkColour(), true); } MarkMenu = s.Sub->AppendSub(AddAmp(LLoadString(IDS_SELECT_MARKED), 's')); if (MarkMenu) { BuildMarkMenu(MarkMenu, MS_Multiple, 0, true, true, true); } s.Sub->AppendSeparator(); s.Sub->AppendItem(AddAmp(LLoadString(IDS_INSPECT), 'i'), IDM_INSPECT, true); s.Sub->AppendItem(LLoadString(IDS_PROPERTIES), IDM_PROPERTIES, true); #ifdef _DEBUG Prof->Add("ScriptCallbacks"); #endif LArray Callbacks; if (App->GetScriptCallbacks(LThingContextMenu, Callbacks)) { LScriptArguments Args(NULL); Args[0] = new LVariant(App); Args[1] = new LVariant(this); Args[2] = new LVariant(&s); for (auto c: Callbacks) App->ExecuteScriptCallback(*c, Args); Args.DeleteObjects(); } m.ToScreen(); int Result; #ifdef _DEBUG Prof.Reset(); #endif int Btn = 0; if (m.Left()) Btn = LSubMenu::BtnLeft; else if (m.Right()) Btn = LSubMenu::BtnRight; Result = s.Sub->Float(p, m.x, m.y); switch (Result) { case IDM_OPEN: { DoUI(); break; } case IDM_DELETE: { LVariant ConfirmDelete = false; App->GetOptions()->GetValue(OPT_ConfirmDelete, ConfirmDelete); if (!ConfirmDelete.CastInt32() || LgiMsg(GetList(), LLoadString(IDS_DELETE_ASK), AppName, MB_YESNO) == IDYES) { if (_GetListItems(Sel, false)) { int Index = -1; LList *TheList = LListItem::Parent; LArray Items; for (auto s: Sel) { Mail *m = dynamic_cast(s); if (m) { if (Index < 0) Index = TheList->IndexOf(m); Items.Add(m); } } GetFolder()->Delete(Items, true); if (Index >= 0) { LListItem *i = TheList->ItemAt(Index); if (i) i->Select(true); } } } break; } case IDM_EXPORT: { ExportAll(Parent, sMimeMessage, NULL); break; } case IDM_REPLY: case IDM_REPLY_ALL: { App->MailReplyTo(this, Result == IDM_REPLY_ALL); SetDirty(false); break; } case IDM_FORWARD: { App->MailForward(this); SetDirty(false); break; } case IDM_BOUNCE: { App->MailBounce(this); SetDirty(false); break; } case IDM_SET_READ: { SetListRead(true); break; } case IDM_SET_UNREAD: { SetListRead(false); break; } case IDM_INSPECT: { OnInspect(); break; } case IDM_PROPERTIES: { OnProperties(); break; } case IDM_SELECT_THREAD: { if (_GetListItems(Sel, false)) { List Thread; for (auto s: Sel) { Mail *m = dynamic_cast(s); if (m) m->GetThread(Thread); } for (auto m: Thread) { m->Select(true); } } break; } case IDM_DELETE_THREAD: { if (_GetListItems(Sel, false)) { List Thread; for (auto s: Sel) { Mail *m = dynamic_cast(s); if (m) m->GetThread(Thread); } for (auto m: Thread) { m->OnDelete(); } } break; } case IDM_IGNORE_THREAD: { if (_GetListItems(Sel, false)) { List Thread; for (auto s: Sel) { Mail *m = dynamic_cast(s); if (m) m->GetThread(Thread); } for (auto m: Thread) { m->SetFlags(m->GetFlags() | MAIL_IGNORE | MAIL_READ); } } break; } case IDM_UNMARK: case IDM_SELECT_NONE: case IDM_SELECT_ALL: default: { if (Result == IDM_UNMARK || (Result >= IDM_MARK_BASE && Result < IDM_MARK_BASE+CountOf(MarkColours32))) { bool Marked = Result != IDM_UNMARK; if (_GetListItems(Sel, false)) { COLOUR Col32 = Marked ? MarkColours32[Result - IDM_MARK_BASE] : 0; for (auto s: Sel) { Mail *m = dynamic_cast(s); if (m) { if (m->SetMarkColour(Col32)) { if (m->GetObject()->GetInt(FIELD_STORE_TYPE) != Store3Imap) // Imap knows to save itself. m->SetDirty(); } m->Update(); } } } } else if (Result == IDM_SELECT_NONE || Result == IDM_SELECT_ALL || (Result >= IDM_MARK_SELECT_BASE && Result < IDM_MARK_SELECT_BASE+CountOf(MarkColours32))) { bool None = Result == IDM_SELECT_NONE; bool All = Result == IDM_SELECT_ALL; uint32_t c32 = MarkColours32[Result - IDM_MARK_SELECT_BASE]; if (_GetListItems(Sel, true)) { for (auto s: Sel) { Mail *m = dynamic_cast(s); if (!m) break; auto CurCol = m->GetMarkColour(); if (None) { m->Select(CurCol <= 0); } else { if (CurCol > 0) { if (All) { m->Select(true); } else { m->Select(CurCol && CurCol == c32); } } else { m->Select(false); } } } } } else if (Result >= AttachBaseMsg && Result < AttachBaseMsg + (ssize_t)AttachLst.Length()) { // save attachment as... Attachment *a = AttachLst.ItemAt(Result - AttachBaseMsg); if (a) { a->OnSaveAs(Parent); } } else { // Handle any installed callbacks for menu items for (unsigned i=0; iExecuteScriptCallback(Cb, Args); } } } break; } } DeleteObj(s.Sub); } } void Mail::OnMouseClick(LMouse &m) { if (m.Down()) { if (!m.IsContextMenu()) { if (m.Double()) { // open the UI for the Item DoUI(); } } else { DoContextMenu(m); } } } void Mail::OnCreate() { LOptionsFile *Options = App->GetOptions(); LVariant v; if (GetObject()) GetObject()->SetInt(FIELD_FLAGS, MAIL_CREATED); // Get identity and set from info ScribeAccount *Ident = App->GetCurrentAccount(); if (Ident) { v = Ident->Identity.Name(); GetFrom()->SetStr(FIELD_NAME, v.Str()); v = Ident->Identity.Email(); GetFrom()->SetStr(FIELD_EMAIL, v.Str()); v = Ident->Identity.ReplyTo(); if (v.Str()) GetReply()->SetStr(FIELD_REPLY, v.Str()); } else { LAssert(!"No identity selected"); } LVariant EditCtrl; App->GetOptions()->GetValue(OPT_EditControl, EditCtrl); LAutoString Sig = GetSig(EditCtrl.CastInt32() != 0, Ident); if (!Sig && EditCtrl.CastInt32()) { Sig = GetSig(false, Ident); SetBody(Sig); } else if (EditCtrl.CastInt32()) SetHtml(Sig); else SetBody(Sig); LVariant ClipRecip; if (Options->GetValue(OPT_RecipientFromClipboard, ClipRecip) && ClipRecip.CastInt32()) { LClipBoard Clip(App); char *Txt = Clip.Text(); if (Txt && strchr(Txt, '@') && strlen(Txt) < 100) { for (char *s=Txt; *s; s++) { if (*s == '\n' || *s == '\r') { *s = 0; } } Mailto mt(App, Txt); for (auto a: mt.To) { LDataPropI *OldA = dynamic_cast(a); if (OldA) { LDataPropI *NewA = GetTo()->Create(GetObject()->GetStore()); if (NewA) { NewA->CopyProps(*OldA); GetTo()->Insert(NewA); } } } mt.To.Empty(); if (mt.Subject && !ValidStr(GetSubject())) { SetSubject(mt.Subject); } /* if (_strnicmp(Txt, MailToStr, 7) == 0) { char *Question = strchr(Txt + 7, '?'); if (Question) { *Question = 0; Question++; int Len = strlen(SubjectStr); if (_strnicmp(Question, SubjectStr, Len) == 0) { Subject = NewStr(Question + Len); } } La->Addr = NewStr(Txt + 7); } else { La->Addr = NewStr(Txt); } To.Insert(La); */ } } Update(); } void Mail::CreateMailHeaders() { LStringPipe Hdrs(256); MailProtocol Protocol; LVariant HideId; App->GetOptions()->GetValue(OPT_HideId, HideId); Protocol.ProgramName = GetFullAppName(!HideId.CastInt32()); LDataI *Obj = GetObject(); if (Obj && ::CreateMailHeaders(App, Hdrs, Obj, &Protocol)) { LAutoString FullHdrs(Hdrs.NewStr()); Obj->SetStr(FIELD_INTERNET_HEADER, FullHdrs); } else LAssert(!"CreateMailHeaders failed."); } bool Mail::OnBeforeSend(ScribeEnvelope *Out) { if (!Out) { LAssert(!"No output envelope."); return false; } // First check the email from address... if (!ValidStr(GetFrom()->GetStr(FIELD_EMAIL))) { LOptionsFile *Options = App->GetOptions(); if (Options) { ScribeAccount *Ident = App->GetAccounts()->ItemAt(App->GetCurrentIdentity()); if (Ident) { LVariant v = Ident->Identity.Email(); GetFrom()->SetStr(FIELD_EMAIL, v.Str()); v = Ident->Identity.Name(); GetFrom()->SetStr(FIELD_NAME, v.Str()); } } } Out->From = GetFromStr(FIELD_EMAIL); LDataIt To = GetTo(); ContactGroup *Group = NULL; for (LDataPropI *t = To->First(); t; t = To->Next()) { LString Addr = t->GetStr(FIELD_EMAIL); if (LIsValidEmail(Addr)) Out->To.New() = Addr; else if ((Group = LookupContactGroup(App, Addr))) { LString::Array a = Group->GetAddresses(); Out->To.Add(a); } } LDataPropI *Root = GetObject()->GetObj(FIELD_MIME_SEG); if (!Root) { LAssert(!"No root element."); return false; } // Check the headers have been created.. if (!Root->GetStr(FIELD_INTERNET_HEADER)) CreateMailHeaders(); Out->MsgId = GetMessageId(true); // Convert the mime stream LMime Mime(ScribeTempPath()); LTempStream Buf(ScribeTempPath()); Store3ToGMime(&Mime, Root); // Do the encode if (!Mime.Text.Encode.Push(&Buf)) { LAssert(!"Mime encode failed."); return false; } Out->References = GetReferences(); Out->FwdMsgId = GetFwdMsgId(); Out->BounceMsgId = GetBounceMsgId(); // Read the resulting string into the output envelope int Sz = (int)Buf.GetSize(); Out->Rfc822.Length(Sz); Buf.SetPos(0); Buf.Read(Out->Rfc822.Get(), Sz); Out->Rfc822.Get()[Sz] = 0; return true; } void Mail::OnAfterSend() { int f = GetFlags(); f |= MAIL_SENT | MAIL_READ; // set sent flag f &= ~MAIL_READY_TO_SEND; // clear read to send flag // LgiTrace("Setting flags: %x\n", f); SetFlags(f); LDateTime n; n.SetNow(); SetDateSent(&n); // LString s = GetDateSent()->Get(); // LgiTrace("Setting sent date: %s\n", s.Get()); Update(); SetDirty(); if (App) { ScribeFolder *Sent = App->GetFolder(FOLDER_SENT, GetObject()); if (Sent) { LArray Items; Items.Add(this); Sent->MoveTo(Items); } } } bool Mail::OnBeforeReceive() { return true; } enum MimeDecodeMode { MODE_FIELDS, MODE_WAIT_MSG, MODE_SEGMENT, MODE_WAIT_TYPE, MODE_WAIT_BOUNDRY }; #define MF_UNKNOWN 1 #define MF_SUBJECT 2 #define MF_TO 3 #define MF_FROM 4 #define MF_REPLY_TO 5 #define MF_CONTENT_TYPE 6 #define MF_CC 7 #define MF_CONTENT_TRANSFER_ENCODING 9 #define MF_DATE_SENT 10 #define MF_PRIORITY 11 #define MF_COLOUR 12 #define MF_DISPOSITIONNOTIFICATIONTO 13 void MungCharset(Mail *Msg, bool &HasRealCs, ScribeAccount *Acc) { if (ValidStr(Msg->GetBodyCharset())) { HasRealCs = true; } if (Acc) { // LCharset *Cs = 0; if (Msg->GetBodyCharset() && stristr(Msg->GetBodyCharset(), "ascii") && Acc->Receive.AssumeAsciiCharset().Str()) { Msg->SetBodyCharset(Acc->Receive.AssumeAsciiCharset().Str()); HasRealCs = false; } else if (!ValidStr(Msg->GetBodyCharset()) || !LGetCsInfo(Msg->GetBodyCharset())) { Msg->SetBodyCharset(Acc->Receive.Assume8BitCharset().Str()); HasRealCs = false; } } } bool Mail::OnAfterReceive(LStreamI *Msg) { if (!Msg) return false; // Clear the codepage setting here so that we can // check later, down the bottom we must set it to // the default if it's not set anywhere along the // way. SetBodyCharset(0); if (GetObject() && !GetObject()->SetRfc822(Msg)) { LAssert(!"Failed to set mime content."); return false; } // Now parse them into the meta fields... GetObject()->ParseHeaders(); ScribeAccount *Acc = GetAccountSentTo(); LAutoString ContentType; // Fill out any Contact's TimeZone if missing LAutoString DateStr(InetGetHeaderField(GetInternetHeader(), "Date")); LDateTime DateObj; if (DateStr && DateObj.Decode(DateStr)) { double SenderTz = DateObj.GetTimeZoneHours(); if (SenderTz != 0.0) { ListAddr *Sender = dynamic_cast(GetFrom()); if (Sender) { auto It = Sender->begin(); if (*It) { Contact *c = (*It)->GetContact(); if (c) { const char *Tz = ""; if (!c->Get(OPT_TimeZone, Tz) || atof(Tz) != SenderTz) { char s[32]; sprintf_s(s, sizeof(s), "%.1f", SenderTz); c->Set(OPT_TimeZone, s); c->SetDirty(true); } } } } } } auto BodyText = GetBody(); if (BodyText) { if (!GetBodyCharset()) { if (Acc && Acc->Receive.Assume8BitCharset().Str()) { SetBodyCharset(Acc->Receive.Assume8BitCharset().Str()); } else { SetBodyCharset("us-ascii"); } } LStringPipe NewBody; LArray Files; if (DecodeUuencodedAttachment(GetObject()->GetStore(), Files, &NewBody, BodyText)) { LDataI *AttachPoint = GetFileAttachPoint(); if (AttachPoint) { for (unsigned i=0; iSetStr(FIELD_MIME_TYPE, sAppOctetStream); Files[i]->Save(AttachPoint); } SetDirty(!TestFlag(GetFlags(), MAIL_ATTACHMENTS)); SetFlags(GetFlags() | MAIL_ATTACHMENTS); LAutoString n(NewBody.NewStr()); SetBody(n); Update(); if (Ui) Ui->OnAttachmentsChange(); } } } LDateTime n; n.SetNow(); SetDateReceived(&n); Update(); SetDirty(); // Call any 'after receive' callbacks LArray Callbacks; if (App->GetScriptCallbacks(LMailOnAfterReceive, Callbacks)) { for (auto c: Callbacks) { if (!c->Func) continue; LVirtualMachine Vm; LScriptArguments Args(&Vm); Args.New() = new LVariant(App); Args.New() = new LVariant(this); App->ExecuteScriptCallback(*c, Args); Args.DeleteObjects(); } } return true; } ThingUi *Mail::GetUI() { return Ui; } LVariant Mail::GetServerUid() { LVariant v; if (GetObject()) { auto Type = (Store3Backend)GetObject()->GetInt(FIELD_STORE_TYPE); if (Type == Store3Imap) v = GetObject()->GetInt(FIELD_SERVER_UID); else v = GetObject()->GetStr(FIELD_SERVER_UID); } else LAssert(!"No object."); return v; } bool Mail::SetServerUid(LVariant &v) { if (!GetObject()) return false; Store3Status s; if (GetObject()->GetInt(FIELD_STORE_TYPE) == Store3Imap) s = GetObject()->SetInt(FIELD_SERVER_UID, v.CastInt32()); else s = GetObject()->SetStr(FIELD_SERVER_UID, v.Str()); return s > Store3Error; } bool Mail::SetObject(LDataI *o, bool IsDestructor, const char *File, int Line) { bool b = LDataUserI::SetObject(o, IsDestructor, File, Line); if (b) { // Clear out stale attachment objects... UnloadAttachments(); } return b; } bool Mail::SetUI(ThingUi *new_ui) { MailUi *NewMailUi = dynamic_cast(new_ui); if (NewMailUi == Ui) return true; if (Ui) { LWindow *w = Ui; Ui->SetItem(0); w->Quit(); LAssert(Ui == NULL); } if (new_ui) { Ui = dynamic_cast(new_ui); if (Ui) Ui->SetItem(this); else LAssert(!"Incorrect object."); } return true; } ThingUi *Mail::DoUI(MailContainer *c) { if (App && !Ui) { if (App->InThread()) { Ui = new MailUi(this, c ? c : GetFolder()); } else { App->PostEvent(M_SCRIBE_OPEN_THING, (LMessage::Param)this, 0); } } if (Ui) { #if WINNATIVE SetActiveWindow(Ui->Handle()); #endif } return Ui; } int Mail::Compare(LListItem *t, ssize_t Field) { Thing *T = (Thing*)t->_UserPtr; Mail *m = T ? T->IsMail() : 0; if (m) { static int Fields[] = {FIELD_FROM, FIELD_SUBJECT, FIELD_SIZE, FIELD_DATE_RECEIVED}; if (Field < 0) { auto i = -Field - 1; if (i >= 0 && i < CountOf(Fields)) { Field = Fields[i]; } } LVariant v1, v2; const char *s1 = "", *s2 = ""; switch (Field) { case 0: { break; } case FIELD_TO: { LDataPropI *a1 = GetTo()->First(); if (a1) { if (a1->GetStr(FIELD_NAME)) s1 = a1->GetStr(FIELD_NAME); else if (a1->GetStr(FIELD_EMAIL)) s1 = a1->GetStr(FIELD_EMAIL); } LDataPropI *a2 = m->GetTo()->First(); if (a2) { if (a2->GetStr(FIELD_NAME)) s2 = a2->GetStr(FIELD_NAME); else if (a2->GetStr(FIELD_EMAIL)) s2 = a2->GetStr(FIELD_EMAIL); } break; } case FIELD_FROM: { LDataPropI *f1 = GetFrom(); LDataPropI *f2 = m->GetFrom(); if (f1->GetStr(FIELD_NAME)) s1 = f1->GetStr(FIELD_NAME); else if (f1->GetStr(FIELD_EMAIL)) s1 = f1->GetStr(FIELD_EMAIL); if (f2->GetStr(FIELD_NAME)) s2 = f2->GetStr(FIELD_NAME); else if (f2->GetStr(FIELD_EMAIL)) s2 = f2->GetStr(FIELD_EMAIL); break; } case FIELD_SUBJECT: { s1 = GetSubject(); s2 = m->GetSubject(); break; } case FIELD_SIZE: { return (int) (TotalSizeof() - m->TotalSizeof()); break; } case FIELD_DATE_SENT: { auto Sent1 = GetDateSent(); auto Sent2 = m->GetDateSent(); if (!Sent1 || !Sent2) break; return Sent1->Compare(Sent2); break; } case FIELD_DATE_RECEIVED: { return GetDateReceived()->Compare(m->GetDateReceived()); break; } case FIELD_PRIORITY: { return GetPriority() - m->GetPriority(); break; } case FIELD_FLAGS: { int Mask = MAIL_FORWARDED | MAIL_REPLIED | MAIL_READ; return (GetFlags() & Mask) - (m->GetFlags() & Mask); break; } case FIELD_LABEL: { if (GetLabel()) s1 = GetLabel(); if (m->GetLabel()) s2 = m->GetLabel(); break; } case FIELD_MESSAGE_ID: { s1 = GetMessageId(); s2 = m->GetMessageId(); break; } case FIELD_FROM_CONTACT_NAME: { s1 = GetFieldText(FIELD_FROM_CONTACT_NAME); s2 = m->GetFieldText(FIELD_FROM_CONTACT_NAME); break; } case FIELD_SERVER_UID: { v1 = GetServerUid(); v2 = m->GetServerUid(); if (v1.Str() && v2.Str()) { s1 = v1.Str(); s2 = v2.Str(); } else { auto diff = v1.CastInt64() - v2.CastInt64(); if (diff < 0) return -1; return diff > 1; } } default: { s1 = GetObject() ? GetObject()->GetStr((int)Field) : 0; s2 = m->GetObject() ? m->GetObject()->GetStr((int)Field) : 0; break; } } const char *Empty = ""; return _stricmp(s1?s1:Empty, s2?s2:Empty); } return -1; } class MailPropDlg : public LDialog { List Lst; LViewI *Table = NULL; struct FlagInfo { int Flag = 0, Ctrl = 0; void Set(int f, int c) { Flag = f; Ctrl = c; } }; LArray Flags; public: MailPropDlg(LView *Parent, List &lst) { SetParent(Parent); Lst = lst; Flags.New().Set(MAIL_SENT, IDC_SENT); Flags.New().Set(MAIL_RECEIVED, IDC_RECEIVED); Flags.New().Set(MAIL_CREATED, IDC_CREATED); Flags.New().Set(MAIL_FORWARDED, IDC_FORWARDED); Flags.New().Set(MAIL_REPLIED, IDC_REPLIED); Flags.New().Set(MAIL_ATTACHMENTS, IDC_HAS_ATTACH); Flags.New().Set(MAIL_READ, IDC_READ); Flags.New().Set(MAIL_READY_TO_SEND, IDC_READY_SEND); if (LoadFromResource(IDD_MAIL_PROPERTIES)) { GetViewById(IDC_TABLE, Table); LAssert(Table != NULL); SetCtrlEnabled(IDC_OPEN_INSPECTOR, Lst.Length() == 1); for (auto &i: Flags) { int Set = 0; for (auto m: Lst) { if (m->GetFlags() & i.Flag) Set++; } if (Set == Lst.Length()) { SetCtrlValue(i.Ctrl, LCheckBox::CheckOn); } else if (Set) { LCheckBox *Cb; if (GetViewById(i.Ctrl, Cb)) Cb->ThreeState(true); SetCtrlValue(i.Ctrl, LCheckBox::CheckPartial); } } SetCtrlEnabled(IDC_HAS_ATTACH, false); char Msg[512] = ""; if (Lst.Length() == 1) { Mail *m = Lst[0]; auto mObj = m->GetObject(); if (mObj) { auto dt = mObj->GetDate(FIELD_DATE_RECEIVED); sprintf_s( Msg, sizeof(Msg), LLoadString(IDS_MAIL_PROPS_DLG), LFormatSize(mObj->GetInt(FIELD_SIZE)).Get(), dt ? dt->Get().Get() : LLoadString(IDS_NONE), mObj->GetStr(FIELD_DEBUG)); SetCtrlName(IDC_MSG_ID, mObj->GetStr(FIELD_MESSAGE_ID)); } } else { sprintf_s(Msg, sizeof(Msg), LLoadString(IDS_MULTIPLE_ITEMS), Lst.Length()); SetCtrlEnabled(IDC_MSG_ID, false); } SetCtrlName(IDC_MAIL_DESCRIPTION, Msg); MoveSameScreen(Parent); } } void OnPosChange() { if (Table) { LRect r = GetClient(); r.Inset(LTableLayout::CellSpacing, LTableLayout::CellSpacing); Table->SetPos(r); } } int OnNotify(LViewI *Ctr, LNotification n) { switch (Ctr->GetId()) { case IDC_OPEN_INSPECTOR: { if (Lst.Length()) Lst[0]->OnInspect(); break; } case IDOK: { for (auto &i: Flags) { auto v = GetCtrlValue(i.Ctrl); LAssert(v >= 0); // the control couldn't be found... check the .lr8 file for (auto m: Lst) { if (v == 1) m->SetFlags(m->GetFlags() | i.Flag); else if (v == 0) m->SetFlags(m->GetFlags() & ~i.Flag); } } // fall thru } case IDCANCEL: { EndModal(Ctr->GetId()); break; } } return 0; } }; void Mail::OnInspect() { new ObjectInspector(App, this); } void Mail::OnProperties(int Tab) { List Sel; if (GetList()->GetSelection(Sel)) { List Lst; for (auto i: Sel) { Lst.Insert(dynamic_cast(i)); } if (Lst[0]) { auto Dlg = new MailPropDlg(GetList(), Lst); Dlg->DoModal([this, Dlg](auto dlg, auto id) { if (id == IDOK) { SetDirty(); Update(); } delete dlg; }); } } } int Mail::GetImage(int SelFlags) { if (TestFlag(GetFlags(), MAIL_READY_TO_SEND) && !TestFlag(GetFlags(), MAIL_SENT)) { return ICON_UNSENT_MAIL; } if (GetFlags() & MAIL_READ) { return (GetFlags() & MAIL_ATTACHMENTS) ? ICON_READ_ATT_MAIL : ICON_READ_MAIL; } else { return (GetFlags() & MAIL_ATTACHMENTS) ? ICON_UNREAD_ATT_MAIL : ICON_UNREAD_MAIL; } return ICON_READ_MAIL; } int *Mail::GetDefaultFields() { return DefaultMailFields; } void Mail::DeleteAsSpam(LView *View) { // Set read.. SetFlags(GetFlags() | MAIL_READ | MAIL_BAYES_SPAM, true); // Remove from NewMail NewMailLst.Delete(this); LVariant DeleteOnServer, DeleteAttachments, SetRead; auto Opts = App->GetOptions(); if (!Opts->GetValue(OPT_BayesDeleteOnServer, DeleteOnServer)) DeleteOnServer = false; if (!Opts->GetValue(OPT_BayesDeleteAttachments, DeleteAttachments)) DeleteAttachments = false; if (!Opts->GetValue(OPT_BayesSetRead, SetRead)) SetRead = false; if (DeleteOnServer.CastBool()) { // Tell the account it's spam... so that any further connects can // delete it off the server. ScribeAccount *a = GetAccountSentTo(); if (a) { auto ServerUid = GetServerUid(); if (ServerUid.Str()) { a->Receive.DeleteAsSpam(ServerUid.Str()); SetServerUid(ServerUid = NULL); } else { LAutoString Uid(InetGetHeaderField(GetInternetHeader(), "X-UIDL")); if (Uid) a->Receive.DeleteAsSpam(Uid); } } else { #if 0 // def _DEBUG if (LgiMsg(Ui?(LView*)Ui:(LView*)App, "Debug: GetAccountSentTo failed. Debug?", AppName, MB_YESNO) == IDYES) { LAssert(0); } #endif } } if (DeleteAttachments.CastBool()) { // Delete all attachments... they're useless and more than likely // just virii anyway. List Files; if (GetAttachments(&Files)) { for (auto a: Files) { DeleteAttachment(a); } Files.Empty(); } } if (SetRead.CastInt32() != 0) { SetFlags(GetFlags() | MAIL_READ); } // Move it to the spam folder if it exists. auto FolderPath = GetFolder()->GetPath(); - LToken Parts(FolderPath, "/"); + auto Parts = FolderPath.SplitDelimit("/"); if (Parts.Length() == 0) LgiMsg(View, "Error: No folder path?", AppName); else { LString SpamPath; LString SpamLeaf = "Spam"; SpamPath.Printf("/%s/%s", Parts[0], SpamLeaf.Get()); ScribeFolder *Spam = App->GetFolder(SpamPath); if (!Spam) { LMailStore *Ms = App->GetMailStoreForPath(FolderPath); if (!Ms) { if (GetFolder()->GetObject()->GetInt(FIELD_STORE_TYPE) == Store3Imap) { Ms = App->GetDefaultMailStore(); if (Ms && Ms->Root) { Spam = Ms->Root->GetSubFolder(SpamLeaf); if (!Spam) Spam = Ms->Root->CreateSubDirectory(SpamLeaf, MAGIC_MAIL); } } else LgiMsg(View, "Error: Couldn't get mail store for '%s'.", AppName, MB_OK, FolderPath.Get()); } else Spam = Ms->Root->CreateSubDirectory("Spam", MAGIC_MAIL); } if (Spam && Spam != GetFolder()) { LArray Items; Items.Add(this); if (!Spam->MoveTo(Items)) { LgiMsg(View, "Error: Couldn't move email to spam folder.", AppName); } } } } void Mail::SetFlagsCache(int64_t NewFlags, bool IgnoreReceipt, bool UpdateScreen) { if (FlagsCache == NewFlags) return; DepthCheck Depth(d->InSetFlagsCache); bool Read1 = TestFlag(FlagsCache, MAIL_READ); bool Read2 = TestFlag(NewFlags, MAIL_READ); bool ChangeRead = Read1 ^ Read2; // LgiTrace("%p::SetFlagsCache: %s -> %s\n", this, EmailFlagsToStr(FlagsCache).Get(), EmailFlagsToStr(StoreFlags).Get()); FlagsCache = NewFlags; if (ChangeRead && App) { if (Read2) { // Becoming read List Objs; Objs.Insert(this); App->OnNewMail(&Objs, false); PreviewCache.DeleteObjects(); // Read receipt if (!IgnoreReceipt && !TestFlag(NewFlags, MAIL_CREATED) && TestFlag(NewFlags, MAIL_READ_RECEIPT)) { LAutoString Header(InetGetHeaderField(GetInternetHeader(), "Disposition-Notification-To")); if (Header) { LAutoString Name, Addr; DecodeAddrName(Header, Name, Addr, 0); if (Addr) { if (LgiMsg( Ui != 0 ? (LView*)Ui : (LView*)App, LLoadString(IDS_RECEIPT_ASK), AppName, MB_YESNO, GetSubject(), GetFrom()->GetStr(FIELD_NAME), GetFrom()->GetStr(FIELD_EMAIL), (char*)Name, (char*)Addr) == IDYES) { Mail *m = new Mail(App); if (m) { m->App = App; LDataPropI *ToAddr = m->GetTo()->Create(m->GetObject()->GetStore()); if (ToAddr) { ToAddr->SetStr(FIELD_NAME, Name); ToAddr->SetStr(FIELD_EMAIL, Addr); m->GetTo()->Insert(ToAddr); } m->OnReceipt(this); m->Save(); App->Send(); } } } } } LVariant Inc; if (App->GetOptions()->GetValue(OPT_BayesIncremental, Inc) && Inc.CastInt32()) { // Incremental bayesian update App->OnBayesianMailEvent(this, BayesMailUnknown, BayesMailHam); } } if (UpdateScreen) { // Changing read status ScribeFolder *t = GetFolder(); if (t) t->OnUpdateUnRead(0, true); } } if (UpdateScreen || ChangeRead) { Update(); } } uint32_t Mail::GetFlags() { if (GetObject()) { // Make sure the objects are in sync... auto StoreFlags = GetObject()->GetInt(FIELD_FLAGS); if (FlagsCache < 0) FlagsCache = StoreFlags; else if (FlagsCache != StoreFlags) SetFlagsCache(StoreFlags, false, true); } return (uint32_t)FlagsCache; } void Mail::SetFlags(ulong NewFlags, bool IgnoreReceipt, bool UpdateScreen) { if (FlagsCache == NewFlags) return; DepthCheck SetFlagsDepth(d->InSetFlags); if (GetObject()) { Store3Status Result = GetObject()->SetInt(FIELD_FLAGS, NewFlags); if (Result == Store3Success && GetObject()->IsOnDisk()) { // Imap mail shouldn't fall in here. SetDirty(); } } else { LAssert(0); return; } SetFlagsCache(NewFlags, IgnoreReceipt, UpdateScreen); } const char *Mail::GetFieldText(int Field) { static char Buf[512]; switch (Field) { case FIELD_TO: { size_t ch = 0; Buf[0] = 0; LDataIt To = GetTo(); for (LDataPropI *a=To->First(); a; a=To->Next()) { if (ch > 0) ch += sprintf_s(Buf+ch, sizeof(Buf)-ch, ", "); auto Name = a->GetStr(FIELD_NAME); auto Addr = a->GetStr(FIELD_EMAIL); auto n = Name ? Name : Addr; if (!n) continue; // Is the buffer too small? size_t n_len = strlen(n); if (ch + n_len + 8 >= sizeof(Buf)) { // Yes... just truncate it with "..." and bail. ch += sprintf_s(Buf+ch, sizeof(Buf)-ch, "..."); break; } ch += sprintf_s(Buf+ch, sizeof(Buf)-ch, "%s", n); LAssert(ch < sizeof(Buf) - 1); } return Buf; break; } case FIELD_FROM_CONTACT_NAME: { static bool InMethod = false; if (!InMethod) { InMethod = true; LDataPropI *la = GetFrom(); if (la) { auto Email = la->GetStr(FIELD_EMAIL); Contact *c = Contact::LookupEmail(Email); if (c) { const char *f = 0, *l = 0; c->Get(OPT_First, f); c->Get(OPT_Last, l); if (f && l) sprintf_s(Buf, sizeof(Buf), "%s %s", f, l); else if (f) strcpy_s(Buf, sizeof(Buf), f); else if (l) strcpy_s(Buf, sizeof(Buf), l); else Buf[0] = 0; InMethod = false; return Buf; } } InMethod = false; } else { LAssert(0); } // fall through } case FIELD_FROM: { LDataPropI *f = GetFrom(); auto n = f->GetStr(FIELD_NAME); if (n) return n; auto e = f->GetStr(FIELD_EMAIL); if (e) return e; break; } case FIELD_SUBJECT: return GetSubject(); case FIELD_SIZE: { if (TotalSizeCache < 0) TotalSizeof(); if (OptionSizeInKiB) { int ch = sprintf_s(Buf, sizeof(Buf), "%.0f KiB", (double)TotalSizeCache/1024.0); if (ch > 4 + 3) { int digits = ch - 4; char *s = Buf + ((digits % 3) ? digits % 3 : 3); char *e = Buf + ch - 4; while (s < e) { memmove(s + 1, s, strlen(s)+1); *s = ','; s += 3; } } } else LFormatSize(Buf, sizeof(Buf), TotalSizeCache); return Buf; } case FIELD_DATE_SENT: { auto DateSent = GetDateSent(); if (DateSent->Year()) { LDateTime dt = *DateSent; if (GetObject() && GetObject()->GetInt(FIELD_STORE_TYPE) == Store3Imap) { ValidateImapDate(dt); } if (AdjustDateTz) { if (dt.GetTimeZone() != LDateTime::SystemTimeZone()) dt.SetTimeZone(LDateTime::SystemTimeZone(), true); } if (ShowRelativeDates) { auto rel = RelativeTime(dt); strcpy_s(Buf, sizeof(Buf), rel ? rel : "#error:RelativeTime"); } else { dt.Get(Buf, sizeof(Buf)); } } else { sprintf_s(Buf, sizeof(Buf), "(%s)", LLoadString(IDS_NONE, "None")); } return Buf; } case FIELD_DATE_RECEIVED: { auto DateReceived = GetDateReceived(); if (DateReceived->Year()) { LDateTime dt = *DateReceived; if (AdjustDateTz) { if (dt.GetTimeZone() != LDateTime::SystemTimeZone()) dt.SetTimeZone(LDateTime::SystemTimeZone(), true); } dt.Get(Buf, sizeof(Buf)); if (ShowRelativeDates) { auto rel = RelativeTime(dt); strcpy_s(Buf, sizeof(Buf), rel ? rel : "#error:RelativeTime"); } else { dt.Get(Buf, sizeof(Buf)); } } else { sprintf_s(Buf, sizeof(Buf), "(%s)", LLoadString(IDS_NONE, "None")); } return Buf; } case FIELD_TEXT: return GetBody(); case FIELD_MESSAGE_ID: return GetMessageId(); case FIELD_INTERNET_HEADER: return GetInternetHeader(); case FIELD_ALTERNATE_HTML: return GetHtml(); case FIELD_LABEL: return GetLabel(); case FIELD_PRIORITY: case FIELD_FLAGS: return 0; case FIELD_SERVER_UID: sprintf_s(Buf, sizeof(Buf), "%i", GetObject() ? (int)GetObject()->GetInt(Field) : -1); return Buf; case FIELD_RECEIVED_DOMAIN: { if (!d->DomainCache) { if (!GetObject()) return NULL; auto hdr = GetObject()->GetStr(FIELD_INTERNET_HEADER); if (!hdr) return NULL; for (auto ln: LString(hdr).SplitDelimit("\n")) { if (ln.Find("Received: from") == 0) { auto p = ln.SplitDelimit(); if (p.Length() > 2) { auto t = p[2]; if (t.Find(".") > 0) d->DomainCache = t.Strip("[]"); } } } } return d->DomainCache; } default: return GetObject() ? GetObject()->GetStr(Field) : NULL; } return 0; } int Mail::DefaultMailFields[] = { /* FIELD_FLAGS, FIELD_PRIORITY, */ FIELD_FROM, FIELD_SUBJECT, FIELD_SIZE, FIELD_DATE_SENT }; const char *Mail::GetText(int i) { if (FieldArray.Length()) { if (i >= 0 && i < (int)FieldArray.Length()) return GetFieldText(FieldArray[i]); } else if (i < CountOf(DefaultMailFields)) { return GetFieldText(DefaultMailFields[i]); } return NULL; } LDataI *Mail::GetFileAttachPoint() { if (!GetObject()) { LAssert(!"No object ptr."); return 0; } auto Store = GetObject()->GetStore(); // Check existing root node for "multipart/mixed"? auto r = dynamic_cast(GetObject()->GetObj(FIELD_MIME_SEG)); if (!r) { // Create one... r = Store->Create(MAGIC_ATTACHMENT); if (!r) { LAssert(!"No MIME segment ptr."); return NULL; } r->SetStr(FIELD_MIME_TYPE, sMultipartMixed); if (!GetObject()->SetObj(FIELD_MIME_SEG, r)) { LAssert(!"Can't set MIME segment."); return NULL; } } auto Mt = r->GetStr(FIELD_MIME_TYPE); if (!Stricmp(Mt, sMultipartMixed)) { // Yes is it mixed... return that... return r; } // No, a different type of segment, make the parent segment a "mixed", and attach the old segment to the mixed auto Mixed = Store->Create(MAGIC_ATTACHMENT); if (!Mixed) { LAssert(!"Failed to create MAGIC_ATTACHMENT."); return NULL; } // Set the type... Mixed->SetStr(FIELD_MIME_TYPE, sMultipartMixed); // Re-parent the content to the mixed if (!r->Save(Mixed)) { LAssert(!"Can't reparent the content into the mixed seg."); return NULL; } // Re-parent the mixed to the mail object if (!Mixed->Save(GetObject())) { LAssert(!"Can't reparent the mixed seg into the mail object."); return NULL; } return Mixed; } bool Mail::AttachFile(Attachment *File) { bool Status = false; if (File && !Attachments.HasItem(File)) { LDataI *AttachPoint = GetFileAttachPoint(); if (AttachPoint) { if (File->GetObject()->Save(AttachPoint)) { File->App = App; File->SetOwner(this); Attachments.Insert(File); Status = true; SetDirty(!TestFlag(GetFlags(), MAIL_ATTACHMENTS)); SetFlags(GetFlags() | MAIL_ATTACHMENTS); Update(); if (Ui) { Ui->OnAttachmentsChange(); } } } else { File->DecRef(); } } return Status; } Attachment *Mail::AttachFile(LView *Parent, const char *FileName) { LString MimeType = ScribeGetFileMimeType(FileName); LVariant Resize = false; if (!Strnicmp(MimeType.Get(), "image/", 6)) { // Check if we have to do a image resize App->GetOptions()->GetValue(OPT_ResizeImgAttachments, Resize); } auto AttachPoint = GetFileAttachPoint(); if (!AttachPoint) { LAssert(!"No attachment point in MIME heirarchy for file"); return NULL; } auto File = new Attachment( App, GetObject()->GetStore()->Create(MAGIC_ATTACHMENT), FileName); if (File) { File->App = App; if (Resize.CastInt32() || File->GetSize() > 0) { File->SetOwner(this); Attachments.Insert(File); if (Resize.CastInt32()) d->AddResizeImage(File); else File->GetObject()->Save(AttachPoint); SetFlags(GetFlags() | MAIL_ATTACHMENTS); Update(); if (Ui) Ui->OnAttachmentsChange(); } else { LgiMsg(Parent, LLoadString(IDS_ERROR_FILE_EMPTY), AppName); File->DecRef(); File = NULL; } } return File; } bool Mail::Save(ScribeFolder *Into) { bool Status = false; if (!Into) { if (GetFolder()) Into = GetFolder(); else Into = App->GetFolder(FOLDER_OUTBOX); } if (Into) { ScribeFolder *Old = GetFolder(); if (!GetFolder()) { SetParentFolder(Into); } else if (GetFolder() != Into) { // If this fails, you should really by using ScribeFolder::MoveTo to move // the item from it's existing location to the new folder... LAssert(!"Use MoveTo instead."); return false; } Store3Status Result = Into->WriteThing(this); if (Result != Store3Error) { Status = true; SetDirty(false); if (Into && Into == App->GetFolder(FOLDER_TEMPLATES, NULL, true)) { // saving into the templates folder... update the menu App->BuildDynMenus(); } if (Status && DropFileName) { FileDev->Delete(DropFileName, false); DropFileName.Reset(); } } else { SetParentFolder(Old); } } // This frees the resizer thread... // so they aren't hanging around pointlessly d->OnSave(); return Status; } struct WrapQuoteBlock { int Depth; LArray Text; }; void WrapAndQuote( LStream &Pipe, const char *Quote, int Wrap, const char *Text, const char *Cp, const char *MimeType) { int IsHtml = MimeType && !_stricmp(MimeType, sTextHtml); LAutoString In((char*) LNewConvertCp("utf-8", Text, Cp ? Cp : (char*)"utf-8")); const char *QuoteStr = Quote; if (In) { size_t QuoteChars = Strlen(QuoteStr); char NewLine[8]; int NewLineLen = sprintf_s(NewLine, sizeof(NewLine), "%s", IsHtml ? "
\n" : "\n"); // Two step process, parse all the input into an array of paragraph blocks and then output each block // in wrapped form LArray Para; // 1) Parse all input into paragraphs char *i = In; while (*i) { // Parse out the next line... char *e = i; while (*e && *e != '\n') e++; ssize_t len = e - i; int depth = 0; for (int n=0; n') depth++; else if (i[n] != ' ' || i[n] != '\t') break; } if (Para.Length()) { // Can this be added to the previous paragraph? WrapQuoteBlock &Prev = Para[Para.Length()-1]; if (Prev.Depth == depth) { // Yes?! Prev.Text.Add(NewStr(i, len)); i = *e ? e + 1 : e; continue; } } // Create a new paragraph WrapQuoteBlock &New = Para.New(); New.Depth = depth; New.Text.Add(NewStr(i, len)); // Move current position to next line i = *e ? e + 1 : e; } // 2) Output all paragraphs const char *PrefixCharacters = "> \t"; for (unsigned n=0; n 0); if (WordLen == 0) break; // This won't end well... if (Wrap > 0) { // Check if we can put this word on the current line without making it longer than 'Wrap' if (CharPos + WordLen > Wrap) { // No it won't fit... so emit [NewLine][QuoteStr][Prefix] Pipe.Write(NewLine, NewLineLen); if (QuoteStr) Pipe.Write(QuoteStr, QuoteChars * sizeof(*QuoteStr)); Pipe.Write(Prefix, PrefixChars * sizeof(*Prefix)); // Now we are ready to write more words... CharPos = QuoteChars + PrefixChars; } // else Yes it fits... } // Write out the word... if (IsHtml && Strnchr(Start, '<', WordLen)) { for (auto *c = Start; c < Start + WordLen; c++) if (*c == '<') Pipe.Print("<"); else if (*c == '>') Pipe.Print(">"); else Pipe.Write(c, sizeof(*c)); } else Pipe.Write(Start, WordLen * sizeof(*Start)); CharPos += WordLen; Start = End; } if (HardNewLine) { Pipe.Write(NewLine, NewLineLen); if (QuoteStr) Pipe.Write(QuoteStr, QuoteChars * sizeof(*QuoteStr)); Pipe.Write(Prefix, PrefixChars * sizeof(*Prefix)); CharPos = QuoteChars + PrefixChars; } } // Finish the paragraph Pipe.Write(NewLine, NewLineLen); } } } void Mail::WrapAndQuote(LStringPipe &p, const char *Quote, int WrapAt) { LString Mem; LAutoString Cp; if (!ValidStr(GetBody())) Mem = HtmlToText(GetHtml(), GetHtmlCharset()); else Cp = GetCharSet(); ::WrapAndQuote(p, Quote, WrapAt > 0 ? WrapAt : 76, Mem ? Mem.Get() : GetBody(), Cp); } LAutoString Mail::GetSig(bool HtmlVersion, ScribeAccount *Account) { LAutoString r; LVariant Xml; if (!Account) Account = GetAccountSentTo(); if (Account) { if (HtmlVersion) Xml = Account->Identity.HtmlSig(); else Xml = Account->Identity.TextSig(); } if (Xml.Str()) { r = App->ProcessSig(this, Xml.Str(), HtmlVersion ? sTextHtml : sTextPlain); } return r; } void Mail::ProcessTextForResponse(Mail *From, LOptionsFile *Options, ScribeAccount *Account) { LStringPipe Temp; LVariant QuoteStr, Quote, WrapAt; Options->GetValue(OPT_QuoteReply, Quote); Options->GetValue(OPT_WrapAtColumn, WrapAt); Options->GetValue(OPT_QuoteReplyStr, QuoteStr); From->WrapAndQuote(Temp, Quote.CastInt32() ? QuoteStr.Str() : NULL, WrapAt.CastInt32()); LAutoString s = From->GetSig(false, Account); if (s) Temp.Write(s, strlen(s)); s.Reset(Temp.NewStr()); SetBody(s); SetBodyCharset("utf-8"); } ScribeAccount *Mail::GetAccountSentTo() { ScribeAccount *Account = App->GetAccountById(GetAccountId()); if (!Account) { // Older style lookup based on to address... for (auto a : *App->GetAccounts()) { LVariant e = a->Identity.Email(); if (ValidStr(e.Str())) { for (LDataPropI *t=GetTo()->First(); t; t=GetTo()->Next()) { auto Addr = t->GetStr(FIELD_EMAIL); if (ValidStr(Addr)) { if (_stricmp(Addr, e.Str()) == 0) { Account = a; break; } } } } } } return Account; } void Mail::OnReceipt(Mail *m) { if (m) { SetFlags(MAIL_CREATED | MAIL_READY_TO_SEND); ScribeAccount *MatchedAccount = m->GetAccountSentTo(); if (!MatchedAccount) { MatchedAccount = App->GetAccounts()->ItemAt(App->GetCurrentIdentity()); } if (MatchedAccount) { GetFrom()->SetStr(FIELD_NAME, MatchedAccount->Identity.Name().Str()); GetFrom()->SetStr(FIELD_EMAIL, MatchedAccount->Identity.Email().Str()); } char Sent[64]; m->GetDateReceived()->Get(Sent, sizeof(Sent)); char Read[64]; LDateTime t; t.SetNow(); t.Get(Read, sizeof(Read)); char s[256]; sprintf_s(s, sizeof(s), LLoadString(IDS_RECEIPT_BODY), GetFrom()->GetStr(FIELD_NAME), GetFrom()->GetStr(FIELD_EMAIL), Sent, m->GetSubject(), Read); SetBody(s); sprintf_s(s, sizeof(s), "Read: %s", m->GetSubject() ? m->GetSubject() : (char*)""); SetSubject(s); } } LString Mail::GetMailRef() { LString Ret; if (auto MsgId = GetMessageId(true)) { LAssert(!strchr(MsgId, '\n')); ScribeFolder *Fld = GetFolder(); LString FldPath; if (Fld) FldPath = Fld->GetPath(); if (FldPath) { LUri u; auto a = u.EncodeStr(MsgId, MsgIdEncodeChars); if (a) Ret.Printf("%s/%s", FldPath.Get(), a.Get()); } else { Ret = MsgId; } } return Ret; } void Mail::OnReply(Mail *m, bool All, bool MarkOriginal) { if (!m) return; SetWillDirty(false); LVariant ReplyAllSetting = MAIL_ADDR_TO; if (All) { App->GetOptions()->GetValue(OPT_DefaultReplyAllSetting, ReplyAllSetting); } if (MarkOriginal) { // source email has been replyed to m->SetFlags(m->GetFlags() | MAIL_READ); } // this email is ready to send SetFlags(GetFlags() | MAIL_READ | MAIL_CREATED); LDataPropI *ToAddr = GetTo()->Create(GetObject()->GetStore()); if (ToAddr) { bool CopyStatus; auto From = m->GetFrom(); auto Reply = m->GetReply(); auto ReplyTo = Reply->GetStr(FIELD_EMAIL); if (ReplyTo) CopyStatus = ToAddr->CopyProps(*Reply); else CopyStatus = ToAddr->CopyProps(*From); LAssert(CopyStatus); ToAddr->SetInt(FIELD_CC, ReplyAllSetting.CastInt32()); GetTo()->Insert(ToAddr); } LVariant UserName, EmailAddr, ReplyTo; ScribeAccount *a = App->GetCurrentAccount(); if (a) { UserName = a->Identity.Name(); EmailAddr = a->Identity.Email(); ReplyTo = a->Identity.ReplyTo(); } else LAssert(!"No current account."); if (All) { for (LDataPropI *a = m->GetTo()->First(); a; a = m->GetTo()->Next()) { bool Same = App->IsMyEmail(a->GetStr(FIELD_EMAIL)); bool AlreadyThere = false; for (LDataPropI *r = GetTo()->First(); r; r=GetTo()->Next()) { if (_stricmp(r->GetStr(FIELD_EMAIL), a->GetStr(FIELD_EMAIL)) == 0) { AlreadyThere = true; break; } } if (!Same && !AlreadyThere) { LDataPropI *New = GetTo()->Create(GetObject()->GetStore()); if (New) { New->SetInt(FIELD_CC, ReplyAllSetting.CastInt32()); New->SetStr(FIELD_EMAIL, a->GetStr(FIELD_EMAIL)); New->SetStr(FIELD_NAME, a->GetStr(FIELD_NAME)); GetTo()->Insert(New); } } } } ScribeAccount *MatchedAccount = m->GetAccountSentTo(); if (MatchedAccount && MatchedAccount->Identity.Email().Str()) { GetFrom()->SetStr(FIELD_NAME, MatchedAccount->Identity.Name().Str()); GetFrom()->SetStr(FIELD_EMAIL, MatchedAccount->Identity.Email().Str()); ReplyTo = MatchedAccount->Identity.ReplyTo(); if (ReplyTo.Str()) { GetReply()->SetStr(FIELD_NAME, MatchedAccount->Identity.Name().Str()); GetReply()->SetStr(FIELD_EMAIL, ReplyTo.Str()); } } else { GetFrom()->SetStr(FIELD_NAME, UserName.Str()); GetFrom()->SetStr(FIELD_EMAIL, EmailAddr.Str()); if (ValidStr(ReplyTo.Str())) { GetReply()->SetStr(FIELD_NAME, UserName.Str()); GetReply()->SetStr(FIELD_EMAIL, ReplyTo.Str()); } } char Re[32]; sprintf_s(Re, sizeof(Re), "%s:", LLoadString(IDS_REPLY_PREFIX)); auto SrcSubject = (m->GetSubject()) ? m->GetSubject() : ""; if (_strnicmp(SrcSubject, Re, strlen(Re)) != 0) { size_t Len = strlen(SrcSubject) + strlen(Re) + 2; char *s = new char[Len]; if (s) { sprintf_s(s, Len, "%s %s", Re, SrcSubject); SetSubject(s); DeleteArray(s); } } else { SetSubject(m->GetSubject()); } const char *EditMimeType = App->EditCtrlMimeType(); LAutoString Xml = App->GetReplyXml(EditMimeType); if (ValidStr(Xml)) { RemoveReturns(Xml); PreviousMail = m; LString Body = App->ProcessReplyForwardTemplate(m, this, Xml, Cursor, EditMimeType); LVariant EditCtrl; if (App->GetOptions()->GetValue(OPT_EditControl, EditCtrl) && EditCtrl.CastInt32()) { SetHtml(Body); } else { SetBody(Body); SetBodyCharset("utf-8"); } PreviousMail = 0; } else { ProcessTextForResponse(m, App->GetOptions(), MatchedAccount); } // Reference SetReferences(0); auto MailRef = m->GetMailRef(); if (MailRef) SetReferences(MailRef); GetMessageId(true); SetWillDirty(true); ScribeFolder *Folder = m->GetFolder(); if (Folder && Folder->IsPublicFolders()) { SetParentFolder(Folder); } } bool Mail::OnForward(Mail *m, bool MarkOriginal, int WithAttachments) { if (!m) { LAssert(!"No mail object."); LgiTrace("%s:%i - No mail object.\n", _FL); return false; } // ask about attachments? List Attach; if (m->GetAttachments(&Attach) && Attach.Length() > 0) { if (WithAttachments < 0) { LView *p = (m->Ui)?(LView*)m->Ui:(LView*)App; int Result = LgiMsg(p, LLoadString(IDS_ASK_FORWARD_ATTACHMENTS), AppName, MB_YESNOCANCEL); if (Result == IDYES) { WithAttachments = 1; } else if (Result == IDCANCEL) { return false; } } } if (MarkOriginal) { // source email has been forwarded auto MailRef = m->GetMailRef(); if (MailRef) SetFwdMsgId(MailRef); } // this email is ready to send SetFlags(GetFlags() | MAIL_CREATED); SetDirty(); LVariant UserName, EmailAddr, ReplyTo; ScribeAccount *a = App->GetCurrentAccount(); if (a) { UserName = a->Identity.Name(); EmailAddr = a->Identity.Email(); ReplyTo = a->Identity.ReplyTo(); } else LAssert(!"No current account."); ScribeAccount *MatchedAccount = m->GetAccountSentTo(); if (MatchedAccount && MatchedAccount->Identity.Email().Str()) { GetFrom()->SetStr(FIELD_NAME, MatchedAccount->Identity.Name().Str()); GetFrom()->SetStr(FIELD_EMAIL, MatchedAccount->Identity.Email().Str()); ReplyTo = MatchedAccount->Identity.ReplyTo(); if (ValidStr(ReplyTo.Str())) { GetReply()->SetStr(FIELD_NAME, MatchedAccount->Identity.Name().Str()); GetReply()->SetStr(FIELD_EMAIL, ReplyTo.Str()); } } else { GetFrom()->SetStr(FIELD_NAME, UserName.Str()); GetFrom()->SetStr(FIELD_EMAIL, EmailAddr.Str()); if (ValidStr(ReplyTo.Str())) { GetReply()->SetStr(FIELD_NAME, UserName.Str()); GetReply()->SetStr(FIELD_EMAIL, ReplyTo.Str()); } } SetBodyCharset(0); char PostFix[32]; sprintf_s(PostFix, sizeof(PostFix), "%s:", LLoadString(IDS_FORWARD_PREFIX)); auto SrcSubject = (m->GetSubject()) ? m->GetSubject() : ""; if (_strnicmp(SrcSubject, PostFix, strlen(PostFix)) != 0) { size_t Len = strlen(SrcSubject) + strlen(PostFix) + 2; char *s = new char[Len]; if (s) { sprintf_s(s, Len, "%s %s", PostFix, SrcSubject); SetSubject(s); DeleteArray(s); } } else { SetSubject(m->GetSubject()); } // Wrap/Quote/Sig the text... const char *EditMimeType = App->EditCtrlMimeType(); LAutoString Xml = App->GetForwardXml(EditMimeType); if (ValidStr(Xml)) { RemoveReturns(Xml); PreviousMail = m; LString Body = App->ProcessReplyForwardTemplate(m, this, Xml, Cursor, EditMimeType); LVariant EditCtrl; if (App->GetOptions()->GetValue(OPT_EditControl, EditCtrl) && EditCtrl.CastInt32()) { SetHtml(Body); } else { SetBody(Body); SetBodyCharset("utf-8"); } PreviousMail = 0; } else { ProcessTextForResponse(m, App->GetOptions(), MatchedAccount); } // Attachments if (WithAttachments > 0) for (auto From: Attach) AttachFile(new Attachment(App, From)); // Set MsgId GetMessageId(true); // Unless the user edits the message it shouldn't be made dirty. SetDirty(false); return true; } bool Mail::OnBounce(Mail *m, bool MarkOriginal, int WithAttachments) { if (m) { if (MarkOriginal) { // source email has been forwarded auto MsgId = m->GetMessageId(true); if (MsgId) { ScribeFolder *Fld = m->GetFolder(); LString FldPath; if (Fld) FldPath = Fld->GetPath(); if (FldPath) { LUri u; LString a = u.EncodeStr(MsgId, MsgIdEncodeChars); if (a) { LString p; p.Printf("%s/%s", FldPath.Get(), a.Get()); SetBounceMsgId(p); } } else { SetBounceMsgId(MsgId); } } else m->SetFlags(m->GetFlags() | MAIL_BOUNCED); } *this = (Thing&)*m; GetTo()->DeleteObjects(); SetFlags(MAIL_READ | MAIL_BOUNCE); return true; } return false; } void Mail::OnMeasure(LPoint *Info) { LListItem::OnMeasure(Info); if (PreviewLines && !(GetFlags() & MAIL_READ) && (!GetObject() || !GetObject()->GetInt(FIELD_DONT_SHOW_PREVIEW))) { LFont *PreviewFont = App->GetPreviewFont(); Info->y += PreviewFont ? PreviewFont->GetHeight() << 1 : 28; } } void Mail::OnPaintColumn(LItem::ItemPaintCtx &Ctx, int i, LItemColumn *c) { int Field = 0; if (FieldArray.Length()) { Field = i >= 0 && i < (int)FieldArray.Length() ? FieldArray[i] : 0; } else if (i >= 0 && i < CountOf(DefaultMailFields)) { Field = DefaultMailFields[i]; } if (Container && i >= 0 && Field == FIELD_SUBJECT) { LFont *f = Parent->GetFont(); auto Subj = GetSubject(); if (Container) { // Container needs to paint the subject... Container->OnPaint(Ctx.pDC, Ctx, c, Ctx.Fore, Ctx.Back, f?f:LSysFont, Subj); } } else { LListItem::OnPaintColumn(Ctx, i, c); if (i >= 0 && App->GetIconImgList()) { int Icon = -1; switch (Field) { case FIELD_PRIORITY: { switch (GetPriority()) { case MAIL_PRIORITY_HIGH: { Icon = ICON_PRIORITY_HIGH; break; } case MAIL_PRIORITY_LOW: { Icon = ICON_PRIORITY_LOW; break; } default: break; } break; } case FIELD_FLAGS: { if (GetFlags() & MAIL_BOUNCED) { Icon = ICON_FLAGS_BOUNCE; } else if (GetFlags() & MAIL_REPLIED) { Icon = ICON_FLAGS_REPLY; } else if (GetFlags() & MAIL_FORWARDED) { Icon = ICON_FLAGS_FORWARD; } break; } default: break; } if (Icon >= 0) { LRect *b = App->GetIconImgList()->GetBounds(); if (b) { b += Icon; int x = Ctx.x1 + ((Ctx.X()-b->X())/2) - b->x1; int y = Ctx.y1 + ((MIN(Ctx.Y(), 16)-b->Y())/2) - b->y1; LColour Back(Ctx.Back); App->GetIconImgList()->Draw(Ctx.pDC, x, y, Icon, Back); } } } } } void Mail::OnPaint(LItem::ItemPaintCtx &InCtx) { LItem::ItemPaintCtx Ctx = InCtx; int64_t MarkCol = GetMarkColour(); if (MarkCol > 0) { LColour Col((uint32_t)MarkCol, 32); LColour CtxBack(Ctx.Back); LColour Mixed = Col.Mix(CtxBack, MarkColourMix); Ctx.Back = Mixed; } if (Parent) ((ThingList*)Parent)->CurrentMail = this; LListItem::OnPaint(Ctx); if (Parent) ((ThingList*)Parent)->CurrentMail = 0; LFont *PreviewFont = App->GetPreviewFont(); LFont *ColumnFont = Parent && Parent->GetFont() ? Parent->GetFont() : LSysFont; if (Parent && PreviewLines && PreviewFont && ColumnFont && GetObject() && !GetObject()->GetInt(FIELD_DONT_SHOW_PREVIEW) && !(GetFlags() & MAIL_READ)) { int y = ColumnFont->GetHeight() + 2; int Space = (Ctx.Y() - y) >> 1; int Line = MAX(PreviewFont->GetHeight(), Space); PreviewFont->Transparent(true); // Setup if (!PreviewCache[0] || abs(PreviewCacheX-Ctx.X()) > 20) { PreviewCache.DeleteObjects(); PreviewCacheX = Ctx.X(); LVariant v; if (GetValue("BodyAsText[1024]", v) && v.Str()) { char16 *Base = Utf8ToWide(v.Str()); char16 *Txt = Base; int i; for (i=0; Txt[i]; i++) { if (Txt[i] < ' ') { Txt[i] = ' '; } } auto TxtLen = StrlenW(Txt); for (i=0; Txt && *Txt && i<2; i++) { LDisplayString *Temp = new LDisplayString(PreviewFont, Txt, MIN(1024, TxtLen)); if (Temp) { int W = Ctx.X()-18; ssize_t Cx = Temp->CharAt(W); if (Cx > 0 && Cx <= TxtLen) { LDisplayString *d = new LDisplayString(PreviewFont, Txt, Cx); if (d) { PreviewCache.Insert(d); } DeleteObj(Temp); Txt += Cx; // LSeekUtf8 TxtLen -= Cx; } else break; } } DeleteArray(Base); } } // Display LColour PreviewCol(App->GetColour(L_MAIL_PREVIEW)); if (Select()) { int GreyPrev = PreviewCol.GetGray(); int GreyBack = Ctx.Back.GetGray(); int d = GreyPrev - GreyBack; if (d < 0) d = -d; if (d < 128) { // too near PreviewFont->Colour(Ctx.Fore, Ctx.Back); } else { // ok PreviewFont->Colour(PreviewCol, Ctx.Back); } } else { PreviewFont->Colour(PreviewCol, Ctx.Back); } for (auto p: PreviewCache) { p->Draw(Ctx.pDC, Ctx.x1+16, Ctx.y1+y, 0); y += Line; } } } bool Mail::GetFormats(bool Export, LString::Array &MimeTypes) { if (Export) { MimeTypes.Add("text/plain"); MimeTypes.Add(sMimeMbox); } MimeTypes.Add(sMimeMessage); return MimeTypes.Length() > 0; } Thing::IoProgress Mail::Import(IoProgressImplArgs) { if (!mimeType) IoProgressError("No mime type."); if (Stricmp(mimeType, sMimeMessage)) IoProgressNotImpl(); // Single email.. OnAfterReceive(stream); int Flags = GetFlags(); Flags &= ~MAIL_CREATED; Flags |= MAIL_READ; SetFlags(Flags); Update(); IoProgressSuccess(); } #define TIMEOUT_OBJECT_LOAD 20000 Thing::IoProgress Mail::Export(IoProgressImplArgs) { if (!mimeType) IoProgressError("No mimetype."); if (!Stricmp(mimeType, "text/plain")) { LStringPipe Buf; char Str[256]; char Eol[] = EOL_SEQUENCE; // Addressee if (GetFlags() & MAIL_CREATED) { Buf.Push("To:"); for (LDataPropI *a=GetTo()->First(); a; a=GetTo()->Next()) { sprintf_s(Str, sizeof(Str), "\t%s <%s>%s", a->GetStr(FIELD_NAME), a->GetStr(FIELD_EMAIL), Eol); Buf.Push(Str); } } else { sprintf_s(Str, sizeof(Str), "From: %s <%s>%s", GetFrom()->GetStr(FIELD_NAME), GetFrom()->GetStr(FIELD_EMAIL), Eol); Buf.Push(Str); } // Subject sprintf_s(Str, sizeof(Str), "Subject: %s%s", GetSubject(), Eol); Buf.Push(Str); // Sent date if (GetDateSent()->Year()) { int ch = sprintf_s(Str, sizeof(Str), "Sent Date: "); GetDateSent()->Get(Str+ch, sizeof(Str)-ch); } else { int ch = sprintf_s(Str, sizeof(Str), "Date Received: "); GetDateReceived()->Get(Str+ch, sizeof(Str)-ch); } strcat(Str, Eol); Buf.Push(Str); // Body Buf.Push(Eol); Buf.Push(GetBody()); // Write the output auto s = Buf.NewLStr(); if (!s) IoProgressError("No data to output."); stream->Write(s.Get(), s.Length()); IoProgressSuccess(); } else if (!Stricmp(mimeType, sMimeMbox)) { char Temp[256]; // generate from header sprintf_s(Temp, sizeof(Temp), "From %s ", GetFrom()->GetStr(FIELD_EMAIL)); struct tm Ft; ZeroObj(Ft); LDateTime Rec = *GetDateReceived(); if (!Rec.Year()) Rec.SetNow(); Ft.tm_sec = Rec.Seconds(); /* seconds after the minute - [0,59] */ Ft.tm_min = Rec.Minutes(); /* minutes after the hour - [0,59] */ Ft.tm_hour = Rec.Hours(); /* hours since midnight - [0,23] */ Ft.tm_mday = Rec.Day(); /* day of the month - [1,31] */ Ft.tm_mon = Rec.Month() - 1; /* months since January - [0,11] */ Ft.tm_year = Rec.Year() - 1900; /* years since 1900 */ Ft.tm_wday = Rec.DayOfWeek(); strftime(Temp+strlen(Temp), MAX_NAME_SIZE, "%a %b %d %H:%M:%S %Y", &Ft); strcat(Temp, "\r\n"); // write mail stream->Write(Temp, strlen(Temp)); auto Status = Export(stream, sMimeMessage, [cb](auto io, auto stream) { if (io->status == Store3Success) stream->Write((char*)"\r\n.\r\n", 2); else if (io->status == Store3Delayed) LAssert(!"We should never get delayed here... it's the callback!"); if (cb) cb(io, stream); }); return Status; } else if (!Stricmp(mimeType, sMimeMessage)) { // This function can't be asynchronous, it must complete with UI or waiting for a callback. // Because it is used by the drag and drop system. Which won't wait. auto state = GetLoaded(); if (state != Store3Loaded) IoProgressError("Mail not loaded."); if (!GetObject()) IoProgressError("No store object."); auto Data = GetObject()->GetStream(_FL); if (!Data) IoProgressError("Mail for export has no data."); Data->SetPos(0); LCopyStreamer Cp(512<<10); if (Cp.Copy(Data, stream) <= 0) IoProgressError("Mail copy stream failed."); IoProgressSuccess(); } IoProgressNotImpl(); } char *Mail::GetNewText(int Max, const char *AsCp) { LAutoString CharSet = GetCharSet(); char *Txt = 0; // This limits the preview of the text to // the first 64kb, which is reasonable. int Len = Max > 0 ? Strnlen(GetBody(), Max) : -1; // Convert or allocate the text. if (CharSet) { Txt = (char*)LNewConvertCp(AsCp, GetBody(), GetBodyCharset(), Len); } else { Txt = NewStr(GetBody(), Len); } return Txt; } LAutoString Mail::GetCharSet() { LAutoString Status; LAutoString ContentType; if (GetBodyCharset()) { // If the charset has a stray colon... char *Colon = strchr(GetBodyCharset(), ';'); // Kill it. if (Colon) *Colon = 0; // Copy the string.. Status.Reset(NewStr(GetBodyCharset())); } if (!GetBodyCharset()) { ContentType.Reset(InetGetHeaderField(GetInternetHeader(), "Content-Type")); if (ContentType) { char *CharSet = stristr(ContentType, "charset="); if (CharSet) { CharSet += 8; if (*CharSet) { if (strchr("\"\'", *CharSet)) { char Delim = *CharSet++; char *e = CharSet; while (*e && *e != Delim) { e++; } Status.Reset(NewStr(CharSet, e - CharSet)); } else { char *e = CharSet; while (*e && (IsDigit(*e) || IsAlpha(*e) || strchr("-_", *e))) { e++; } Status.Reset(NewStr(CharSet, e - CharSet)); } } } } /* // If it's not a valid charset... if (!LGetCsInfo(Status)) { // Then kill it. Status.Reset(); if (GetBodyCharset()) { SetBodyCharset(0); SetDirty(); } } */ } return Status; } /* Old Body Printing Code int Lines = 0; char *n; for (n=Body.Str(); n && *n; n++) { if (*n == '\n') Lines++; } char *c = Body.Str(); if (c) { c = WrapLines(c, c ? strlen(c) : 0, 76); Lines = 0; for (n=c; *n; n++) { if (*n == '\n') Lines++; } while (c && *c) { if (Cy + Line > r.y2) { pDC->EndPage(); if (Window->GetMaxPages() > 0 && ++Page >= Window->GetMaxPages()) { break; } pDC->StartPage(); Cy = 0; } char *Eol = strchr(c, '\n'); int Len = 0; int Ret = 0; if (Eol) { Len = (int) Eol - (int) c; if (Len > 0 && c[Len-1] == '\r') { Len--; Ret = 1; } } else { Len = strlen(c); } MailFont->Text(pDC, r.x1, Cy, c, Len); Cy += Line; c += Len + Ret; if (*c == '\n') c++; } } */ int Mail::OnNotify(LViewI *Ctrl, LNotification n) { switch (Ctrl->GetId()) { case IDC_TEXT: { LDocView *Doc = dynamic_cast(Ctrl); if (Doc) { if (n.Type == LNotifyShowImagesChanged) { if (Doc->GetLoadImages() ^ TestFlag(GetFlags(), MAIL_SHOW_IMAGES)) { if (Doc->GetLoadImages()) { SetFlags(GetFlags() | MAIL_SHOW_IMAGES); } else { SetFlags(GetFlags() & ~MAIL_SHOW_IMAGES); } } } if (n.Type == LNotifyFixedWidthChanged) { bool DocFixed = Doc->GetFixedWidthFont(); bool MailFixed = TestFlag(GetFlags(), MAIL_FIXED_WIDTH_FONT); if (DocFixed ^ MailFixed) { if (Doc->GetFixedWidthFont()) { SetFlags(GetFlags() | MAIL_FIXED_WIDTH_FONT); } else { SetFlags(GetFlags() & ~MAIL_FIXED_WIDTH_FONT); } } } if (n.Type == LNotifyCharsetChanged && dynamic_cast(Doc)) { char s[256]; sprintf_s(s, sizeof(s), ">%s", Doc->GetCharset()); SetHtmlCharset(s); } } break; } } return 0; } void Mail::OnPrintHeaders(ScribePrintContext &Context) { char Str[256]; // print document LDrawListSurface *Page = Context.Pages.Last(); int Line = Context.AppFont->GetHeight(); Page->SetFont(Context.AppFont); LDisplayString *Ds = Context.Text(LLoadString(IDS_MAIL_MESSAGE)); Context.CurrentY += Line; LDataPropI *Ad = GetTo()->First(); if (Ad) { sprintf_s(Str, sizeof(Str), "%s: ", LLoadString(FIELD_TO)); Ds = Page->Text(Context.MarginPx.x1, Context.CurrentY, Str); int x = Ds->X(); for (; Ad; Ad = GetTo()->Next()) { if (Ad->GetStr(FIELD_EMAIL) && Ad->GetStr(FIELD_NAME)) { sprintf_s(Str, sizeof(Str), "%s <%s>", Ad->GetStr(FIELD_NAME), Ad->GetStr(FIELD_EMAIL)); } else if (Ad->GetStr(FIELD_EMAIL)) { strcpy_s(Str, sizeof(Str), Ad->GetStr(FIELD_EMAIL)); } else if (Ad->GetStr(FIELD_EMAIL)) { strcpy_s(Str, sizeof(Str), Ad->GetStr(FIELD_EMAIL)); } else continue; Ds = Page->Text(Context.MarginPx.x1 + x, Context.CurrentY, Str); Context.CurrentY += Ds->Y(); } } if (GetFrom()->GetStr(FIELD_EMAIL) || GetFrom()->GetStr(FIELD_NAME)) { sprintf_s(Str, sizeof(Str), "%s: ", LLoadString(FIELD_FROM)); Ds = Page->Text(Context.MarginPx.x1, Context.CurrentY, Str); int x = Ds->X(); if (GetFrom()->GetStr(FIELD_EMAIL) && GetFrom()->GetStr(FIELD_NAME)) { sprintf_s(Str, sizeof(Str), "%s <%s>", GetFrom()->GetStr(FIELD_NAME), GetFrom()->GetStr(FIELD_EMAIL)); } else if (GetFrom()->GetStr(FIELD_EMAIL)) { strcpy_s(Str, sizeof(Str), GetFrom()->GetStr(FIELD_EMAIL)); } else if (GetFrom()->GetStr(FIELD_NAME)) { strcpy_s(Str, sizeof(Str), GetFrom()->GetStr(FIELD_NAME)); } else Str[0] = 0; Ds = Page->Text(Context.MarginPx.x1 + x, Context.CurrentY, Str); Context.CurrentY += Ds->Y(); } { // Subject LString s; const char *Subj = GetSubject(); s.Printf("%s: %s", LLoadString(FIELD_SUBJECT), Subj?Subj:""); Context.Text(s); } { // Date LString s; GetDateSent()->Get(Str, sizeof(Str)); s.Printf("%s: %s", LLoadString(IDS_DATE), Str); Context.Text(s); } // attachment list... List Attached; if (GetAttachments(&Attached) && Attached.Length() > 0) { sprintf_s(Str, sizeof(Str), "%s: ", LLoadString(IDS_ATTACHMENTS)); Ds = Page->Text(Context.MarginPx.x1, Context.CurrentY, Str); int x = Ds->X(); for (auto a: Attached) { char Size[32]; LFormatSize(Size, sizeof(Size), a->GetSize()); sprintf_s(Str, sizeof(Str), "%s (%s)", a->GetName(), Size); Ds = Page->Text(Context.MarginPx.x1 + x, Context.CurrentY, Str); Context.CurrentY += Ds->Y(); } } // separator line LRect Sep(Context.MarginPx.x1, Context.CurrentY + (Line * 5 / 10), Context.MarginPx.x2, Context.CurrentY + (Line * 6 / 10)); Page->Rectangle(&Sep); Context.CurrentY += Line; } void Mail::OnPrintText(ScribePrintContext &Context, LPrintPageRanges &Pages) { // Text printing LDrawListSurface *Page = Context.Pages.Last(); LVariant Body; if (!GetVariant("BodyAsText", Body) || !Body.Str()) { LgiTrace("%s:%i - No content to print.\n", _FL); return; } LAutoWString w(Utf8ToWide(Body.Str())); if (!w) { LgiTrace("%s:%i - Utf8ToWide failed.\n", _FL); return; } Page->SetFont(Context.MailFont); for (char16 *s = w; s && *s; ) { // Find end of line.. char16 *n = StrchrW(s, '\n'); if (!n) n = s + StrlenW(s); ssize_t LineLen = n - s; // Find how many characters will fit on the page ssize_t Fit = 0; if (n > s) { LDisplayString a(Context.MailFont, s, MIN(LineLen, 1024)); Fit = a.CharAt(Context.MarginPx.X()); } if (Fit < 0) { break; } char16 *e = s + Fit; if (e < n) { // The whole line doesn't fit on the page... // Find the best breaking opportunity before that... #define ExtraBreak(c) ( ( (c) >= 0x3040 && (c) <= 0x30FF ) || \ ( (c) >= 0x3300 && (c) <= 0x9FAF ) \ ) char16 *StartE = e; while (e > s) { if (e[-1] == ' ' || ExtraBreak(e[-1])) { break; } e--; } if (e == s) { e = StartE; } } // Output the segment of text bool HasRet = e > s ? e[-1] == '\r' : false; LString Str(s, (e - s) - (HasRet ? 1 : 0)); Context.Text(Str); // Update the pointers s = e; if (*s == '\n') s++; } } /// \returns the number of pages printed int Mail::OnPrintHtml(ScribePrintContext &Context, LPrintPageRanges &Pages, LSurface *RenderedHtml) { // HTML printing... LDrawListSurface *Page = Context.Pages.Last(); // Work out the scaling from memory bitmap to page.. double MemScale = (double) Context.pDC->X() / (double) RenderedHtml->X(); // Now paint the bitmap onto the existing page int PageIdx = 0, Printed = 0; for (int y = 0; y < RenderedHtml->Y(); PageIdx++) { if (Pages.InRanges(PageIdx)) { // Work out how much bitmap we can paint onto the current page... int PageRemaining = Context.MarginPx.Y() - Context.CurrentY; int MemPaint = (int) (PageRemaining / MemScale); // This is how much of the memory context we can fit on the page LRect MemRect(0, y, RenderedHtml->X()-1, y + MemPaint - 1); LRect Bnds = RenderedHtml->Bounds(); MemRect.Bound(&Bnds); // Work out how much page that is take up int PageHeight = (int) (MemRect.Y() * MemScale); // This is the position on the page we are blting to LRect PageRect(Context.MarginPx.x1, Context.CurrentY, Context.MarginPx.x2, Context.CurrentY + PageHeight - 1); // Do the blt Page->StretchBlt(&PageRect, RenderedHtml, &MemRect); Printed++; // Now move our position down the page.. Context.CurrentY += PageHeight; if ((Context.MarginPx.Y() - Context.CurrentY) * 100 / Context.MarginPx.Y() < 5) { // Ok we hit the end of the page and need to go to the next page Context.CurrentY = Context.MarginPx.y1; Page = Context.NewPage(); } y += MemRect.Y(); } } return Printed; } ////////////////////////////////////////////////////////////////////////////// size_t Mail::Length() { size_t Status = 0; for (auto a: Attachments) { if (a->GetMimeType() && _stricmp(a->GetMimeType(), sMimeMessage) == 0) { Status++; } } return Status; } ssize_t Mail::IndexOf(Mail *m) { ssize_t Status = -1; ssize_t n = 0; for (auto a: Attachments) { if (a->GetMimeType() && _stricmp(a->GetMimeType(), sMimeMessage) == 0) { if (a->GetMsg() == m) { Status = n; break; } n++; } } return Status; } Mail *Mail::operator [](size_t i) { size_t n = 0; for (auto a: Attachments) { if (a->GetMimeType() && _stricmp(a->GetMimeType(), sMimeMessage) == 0) { if (n == i) return a->GetMsg(); n++; } } return NULL; } bool Mail::LoadFromFile(char *File) { if (File) { LAutoPtr f(new LFile); if (f->Open(File, O_READ)) { return Import(AutoCast(f), sMimeMessage); } } return false; } /////////////////////////////////////////////////////////////////////////////////////////////////////// bool CreateMailAddress(LStream &Out, LDataPropI *Addr, MailProtocol *Protocol) { if (!Addr) return false; auto Name = Addr->GetStr(FIELD_NAME); auto Email = Addr->GetStr(FIELD_EMAIL); if (!Email) return false; Name = EncodeRfc2047(NewStr(Name), 0, &Protocol->CharsetPrefs); if (Name) { if (strchr(Name, '\"')) Out.Print("'%s' ", Name); else Out.Print("\"%s\" ", Name); DeleteArray(Name); } Out.Print("<%s>", Email); return true; } bool CreateMailHeaders(ScribeWnd *App, LStream &Out, LDataI *Mail, MailProtocol *Protocol) { bool Status = true; // Setup char Buffer[1025]; // Construct date LDateTime Dt; int TimeZone = Dt.SystemTimeZone(); Dt.SetNow(); sprintf_s(Buffer, sizeof(Buffer), "Date: %s, %i %s %i %i:%2.2i:%2.2i %s%2.2d%2.2d\r\n", LDateTime::WeekdaysShort[Dt.DayOfWeek()], Dt.Day(), LDateTime::MonthsShort[Dt.Month()-1], Dt.Year(), Dt.Hours(), Dt.Minutes(), Dt.Seconds(), (TimeZone >= 0) ? "+" : "", TimeZone / 60, abs(TimeZone) % 60); Status &= Out.Write(Buffer, strlen(Buffer)) > 0; if (Protocol && Protocol->ProgramName) { // X-Mailer: Status &= Out.Print("X-Mailer: %s\r\n", Protocol->ProgramName.Get()) > 0; } if (Protocol && Protocol->ExtraOutgoingHeaders) { for (char *s=Protocol->ExtraOutgoingHeaders; s && *s; ) { char *e = s; while (*e && *e != '\r' && *e != '\n') e++; ssize_t l = e-s; if (l > 0) { Status &= Out.Write(s, l) > 0; Status &= Out.Write((char*)"\r\n", 2) > 0; } while (*e && (*e == '\r' || *e == '\n')) e++; s = e; } } int Priority = (int)Mail->GetInt(FIELD_PRIORITY); if (Priority != MAIL_PRIORITY_NORMAL) { // X-Priority: Status &= Out.Print("X-Priority: %i\r\n", Priority) > 0; } uint32_t MarkColour = (uint32_t)Mail->GetInt(FIELD_COLOUR); if (MarkColour) { // X-Color (HTML Colour Ref for email marking) Status &= Out.Print("X-Color: #%2.2X%2.2X%2.2X\r\n", R24(MarkColour), G24(MarkColour), B24(MarkColour)) > 0; } // Message-ID: auto MessageID = Mail->GetStr(FIELD_MESSAGE_ID); if (MessageID) { for (auto m=MessageID; *m; m++) { if (*m <= ' ') { printf("%s:%i - Bad message ID '%s'\n", _FL, MessageID); return false; } } Status &= Out.Print("Message-ID: %s\r\n", MessageID) > 0; } // References: auto References = Mail->GetStr(FIELD_REFERENCES); if (ValidStr(References)) { auto Dir = strrchr(References, '/'); LString Ref; if (Dir) { LUri u; Ref = u.DecodeStr(Dir + 1); } else Ref = References; if (*Ref == '<') Status &= Out.Print("References: %s\r\n", Ref.Get()) > 0; else Status &= Out.Print("References: <%s>\r\n", Ref.Get()) > 0; } // To: LDataIt Addr = Mail->GetList(FIELD_TO); LArray Objs; LArray To, Cc; ContactGroup *Group; for (unsigned i=0; iLength(); i++) { LDataPropI *a = (*Addr)[i]; EmailAddressType AddrType = (EmailAddressType)a->GetInt(FIELD_CC); LString Addr = a->GetStr(FIELD_EMAIL); if (LIsValidEmail(Addr)) { if (AddrType == MAIL_ADDR_CC) Cc.Add(a); else if (AddrType == MAIL_ADDR_TO) To.Add(a); } else if ((Group = LookupContactGroup(App, Addr))) { Group->UsedTs.SetNow(); LString::Array Addrs = Group->GetAddresses(); for (unsigned n=0; nGetObject()->GetStore(), NULL); if (sa) { sa->Addr = Addrs[n]; Objs.Add(sa); if (AddrType == MAIL_ADDR_CC) Cc.Add(sa); else if (AddrType == MAIL_ADDR_TO) To.Add(sa); } } } } if (To.Length()) { for (unsigned i=0; iGetObj(FIELD_FROM); if (From && From->GetStr(FIELD_EMAIL)) { Out.Print("From: "); if (!CreateMailAddress(Out, From, Protocol)) return false; Out.Print("\r\n"); } else { LgiTrace("%s:%i - No from address.\n", _FL); return false; } // Reply-To: LDataPropI *Reply = Mail->GetObj(FIELD_REPLY); if (Reply && ValidStr(Reply->GetStr(FIELD_EMAIL))) { Out.Print("Reply-To: "); if (!CreateMailAddress(Out, Reply, Protocol)) return false; Out.Print("\r\n"); } // Subject: char *Subj = EncodeRfc2047(NewStr(Mail->GetStr(FIELD_SUBJECT)), 0, &Protocol->CharsetPrefs, 9); sprintf_s(Buffer, sizeof(Buffer), "Subject: %s\r\n", (Subj) ? Subj : ""); Status &= Out.Write(Buffer, strlen(Buffer)) > 0; DeleteArray(Subj); // DispositionNotificationTo uint8_t DispositionNotificationTo = TestFlag(Mail->GetInt(FIELD_FLAGS), MAIL_READ_RECEIPT); if (DispositionNotificationTo && From) { int ch = sprintf_s(Buffer, sizeof(Buffer), "Disposition-Notification-To:"); char *Nme = EncodeRfc2047(NewStr(From->GetStr(FIELD_NAME)), 0, &Protocol->CharsetPrefs); if (Nme) { ch += sprintf_s(Buffer+ch, sizeof(Buffer)-ch, " \"%s\"", Nme); DeleteArray(Nme); } ch += sprintf_s(Buffer+ch, sizeof(Buffer)-ch, " <%s>\r\n", From->GetStr(FIELD_EMAIL)); Status &= Out.Write(Buffer, ch) > 0; } // Content-Type LDataPropI *Root = Mail->GetObj(FIELD_MIME_SEG); if (Root) { auto MimeType = Root->GetStr(FIELD_MIME_TYPE); auto Charset = Root->GetStr(FIELD_CHARSET); if (MimeType) { LString s; s.Printf("Content-Type: %s%s%s\r\n", MimeType, Charset?"; charset=":"", Charset?Charset:""); Status &= Out.Write(s, s.Length()) == s.Length(); } } else LAssert(0); Objs.DeleteObjects(); return Status; } diff --git a/Code/ScribeSendReceive.cpp b/Code/ScribeSendReceive.cpp --- a/Code/ScribeSendReceive.cpp +++ b/Code/ScribeSendReceive.cpp @@ -1,2848 +1,2848 @@ /* ** FILE: ScribeAccount.cpp ** AUTHOR: Matthew Allen ** DATE: 19/10/99 ** DESCRIPTION: Scribe account ** ** Copyright (C) 2002, Matthew Allen ** fret@memecode.com */ // Includes #include #include #include #include #include "Scribe.h" #include "lgi/common/OpenSSLSocket.h" #include "ScribePrivate.h" #include "lgi/common/NetTools.h" #include "resdefs.h" #include "lgi/common/LgiRes.h" #include "ScribeAccountPreview.h" #include "ScribeUtils.h" #include "lgi/common/TextConvert.h" #define MAIL_POSTED_TO_GUI 0x0001 #define MAIL_EXPLICIT 0x0002 #define SECONDS(i) ((i)*1000) #define TRANSFER_WAIT_TIMEOUT SECONDS(30) #define DEFAULT_SOCKET_TIMEOUT SECONDS(15) LColour SocketMsgTypeToColour(LSocketI::SocketMsgType flags) { LColour col; switch (flags) { case LSocketI::SocketMsgInfo: case LSocketI::SocketMsgNone: col.Set(MAIL_INFO_COLOUR, 24); break; case LSocketI::SocketMsgSend: col.Set(MAIL_SEND_COLOUR, 24); break; case LSocketI::SocketMsgReceive: col.Set(MAIL_RECEIVE_COLOUR, 24); break; case LSocketI::SocketMsgError: col.Set(MAIL_ERROR_COLOUR, 24); break; case LSocketI::SocketMsgWarning: col.Set(MAIL_WARNING_COLOUR, 24); break; default: LAssert(0); break; } return col; } int MakeOpenFlags(ScribeAccount *a, bool Send) { int OpenFlags = 0; Accountlet *l = Send ? (Accountlet*)&a->Send : (Accountlet*)&a->Receive; auto Nam = a->Identity.Name(); auto Em = a->Identity.Email(); // auto SslSmtp = a->Send.UseSSL(); ScribeSslMode SslMode = (ScribeSslMode)l->UseSSL(); switch (SslMode) { case SSL_STARTTLS: OpenFlags |= MAIL_USE_STARTTLS; break; case SSL_DIRECT: OpenFlags |= MAIL_SSL; break; default: break; } if (!Send && a->Receive.SecureAuth()) { OpenFlags |= MAIL_SECURE_AUTH; } if (Send) { if (a->Send.RequireAuthentication()) { OpenFlags |= MAIL_USE_AUTH; } switch (a->Send.AuthType()) { case 1: OpenFlags |= MAIL_USE_PLAIN; break; case 2: OpenFlags |= MAIL_USE_LOGIN; break; case 3: OpenFlags |= MAIL_USE_CRAM_MD5; break; case 4: OpenFlags |= MAIL_USE_OAUTH2; break; } } else { switch (a->Receive.AuthType()) { case 1: OpenFlags |= MAIL_USE_PLAIN; break; case 2: OpenFlags |= MAIL_USE_LOGIN; break; case 3: OpenFlags |= MAIL_USE_NTLM; break; case 4: OpenFlags |= MAIL_USE_OAUTH2; break; } } return OpenFlags; } /* class LProtocolLogger : public LStreamI { LViewI *Wnd; int Msg; List *Log; public: LProtocolLogger(LViewI *wnd, int msg, List *log) { Wnd = wnd; Msg = msg; Log = log; } ssize_t Read(void *Buffer, ssize_t Size, int Flags = 0) { return -1; } ssize_t Write(const void *b, ssize_t len, int f = 0) { LogEntry *l = new LogEntry(SocketMsgTypeToColour((GSocketI::SocketMsgType)f)); if (l) { l->Add((const char*)b, len); Wnd->PostEvent(Msg, (LMessage::Param)Log, (LMessage::Param)l); } return len; } }; */ void MakeTempPath(char *Path, int PathSize) { // Create unique temporary filename char r[32]; do { sprintf_s(r, sizeof(r), "%X.eml", LRand()); LMakePath(Path, PathSize, ScribeTempPath(), r); } while (LFileExists(Path)); } const char *AccountThreadStateName(AccountThreadState i) { switch (i) { case ThreadIdle: return "ThreadIdle"; case ThreadSetup: return "ThreadSetup"; case ThreadConnecting: return "ThreadConnecting"; case ThreadTransfer: return "ThreadTransfer"; case ThreadWaiting: return "ThreadWaiting"; case ThreadDeleting: return "ThreadDeleting"; case ThreadDone: return "ThreadDone"; case ThreadCancel: return "ThreadCancel"; case ThreadError: return "ThreadError"; } return "(none)"; } const char *ReceiveActionName(ReceiveAction i) { switch (i) { case MailNoop: return "MailNoop"; case MailDelete: return "MailDelete"; case MailDownloadAndDelete: return "MailDownloadAndDelete"; case MailDownload: return "MailDownload"; case MailUpload: return "MailUpload"; case MailHeaders: return "MailHeaders"; default: break; } return "(error)"; } const char *ReceiveStatusName(ReceiveStatus i) { switch (i) { case MailReceivedNone: return "ReceiveNone"; case MailReceivedWaiting: return "ReceiveWaiting"; case MailReceivedOk: return "ReceiveOk"; case MailReceivedError: return "ReceiveError"; default: break; } return "(none)"; } bool Accountlet::WaitForTransfers(List &Files) { LArray Counts; auto StartTs = LCurrentTime(); do { Counts.Length(0); for (auto e: Files) Counts[e->Status]++; LSleep(100); if (LCurrentTime() - StartTs > TRANSFER_WAIT_TIMEOUT) { LgiTrace("%s:%i - WaitForTransfers timed out.\n", _FL); for (ReceiveStatus rs = MailReceivedNone; rs < MailReceivedMax; rs = (ReceiveStatus)(((int)rs)+1)) LgiTrace("...%s=%i\n", ReceiveStatusName(rs), Counts[rs]); return false; } else if (Thread && Thread->IsCancelled()) { LgiTrace("%s:%i - WaitForTransfers exiting because thread is cancelled.\n", _FL); return false; } } while (Counts[MailReceivedWaiting] > 0); return true; } //////////////////////////////////////////////////////////////////////////// char *NewStrCat(char *a, char *b) { size_t Len = ((a)?strlen(a):0) + ((b)?strlen(b):0); char *s = new char[Len+1]; if (s) { s[0] = 0; if (a) strcat(s, a); if (b) strcat(s, b); } DeleteArray(a); return s; } class AccountletThread : public AccountThread { friend class SendAccountlet; friend class ReceiveAccountlet; friend bool AfterReceived(MailTransaction *Trans, void *Data); List Files; void MakeTempPath(char *Buf); void SetState(AccountThreadState s); AccountThreadState GetState(); public: // Object - Api AccountletThread(Accountlet *acc, List *Input); ~AccountletThread(); // Methods void Delete(int Index); void Complete(); // Called by the app when the all the files have been processed // void Cancel(); // Called by the app to signal the user is canceling the transfer // Thread int Main(); }; //////////////////////////////////////////////////////////////////////////// AccountletThread::AccountletThread(Accountlet *acc, List *Input) : AccountThread(acc) { } AccountletThread::~AccountletThread() { Files.DeleteObjects(); } void AccountletThread::SetState(AccountThreadState s) { Acc->State = s; } AccountThreadState AccountletThread::GetState() { return Acc->GetState(); } void AccountletThread::Delete(int Index) { MailTransferEvent *e = Files[Index]; if (e) { e->Action = MailDelete; } } void AccountletThread::Complete() { for (auto f: Files) { f->Rfc822Msg.Reset(); } Acc->State = ThreadDeleting; } int AccountletThread::Main() { // We're running... SetState(ThreadSetup); // Do all the connect, transfer and clean up Acc->Main(this); // This tells the app we're all done SetState(ThreadIdle); return 0; } //////////////////////////////////////////////////////////////////////////// LList *MailTransferEvent::GetList() { if (!Account) { LAssert(!"No account."); return NULL; } // As the list is set in the GUI thread, we should only ever use it in the // GUI thread... so check for that here. Otherwise we need to lock it while it's // in use. Which is not currently done. #ifdef _DEBUG ScribeWnd *App = Account->GetApp(); #endif LAssert(App->InThread()); ReceiveAccountlet *ra = dynamic_cast(Account); return ra ? ra->GetItems() : NULL; } //////////////////////////////////////////////////////////////////////////// Accountlet::Accountlet(ScribeAccount *a) : PrivLock("Accountlet") { DataStore = NULL; MailStore = NULL; Root = 0; Account = a; ConnectionStatus = true; OptPassword = 0; Client = 0; LastOnline = 0; Parent = 0; State = ThreadIdle; // Update sig to new format... char Buf[256]; LVariant Old; if (GetApp()->GetOptions()->GetValue(OptionName("AccSig", Buf, sizeof(Buf)), Old)) { if (LFileExists(Old.Str())) { char *Xml = LReadTextFile(Old.Str()); if (Xml) { GetAccount()->Identity.TextSig(Xml); DeleteArray(Xml); } } GetApp()->GetOptions()->DeleteValue(Buf); } } Accountlet::~Accountlet() { I Lck = Lock(_FL); if (Lck) { Lck->d->Log.DeleteObjects(); Lck.Reset(); } DeleteObj(Root); DeleteObj(DataStore); } void Accountlet::OnBeforeDelete() { // This has to be before ~ScribeAccount is finished, so that the // account's index is still valid, if we are going to save the "Expanded" // setting. LString p; if (DataStore && Root) { p = Root->GetPath(); bool e = Root->Expanded(); Expanded(e); } } bool Accountlet::GetVariant(const char *Name, LVariant &Value, const char *Array) { char k[128]; bool mapped = Account->IsMapped(OptionName(Name, k, sizeof(k))); auto Opts = GetApp()->GetOptions(); Value.Empty(); if (mapped) Opts->GetValue(k, Value); return true; } bool Accountlet::SetVariant(const char *Name, LVariant &Value, const char *Array) { char k[128]; bool mapped = Account->IsMapped(OptionName(Name, k, sizeof(k))); auto Opts = GetApp()->GetOptions(); if (mapped) Opts->SetValue(k, Value); return true; } void Accountlet::StrOption(const char *Opt, LVariant &v, const char *Set) { char Key[128]; OptionName(Opt, Key, sizeof(Key)); if (Set) GetApp()->GetOptions()->SetValue(Key, v = Set); else GetApp()->GetOptions()->GetValue(Key, v); } void Accountlet::IntOption(const char *Opt, LVariant &v, int Set) { char Key[128]; OptionName(Opt, Key, sizeof(Key)); if (Set >= 0) GetApp()->GetOptions()->SetValue(Key, v = Set); else GetApp()->GetOptions()->GetValue(Key, v); } const char *Accountlet::GetStateName() { switch (State) { case ThreadIdle: return LLoadString(IDS_ACCSTAT_IDLE); case ThreadSetup: return LLoadString(IDS_ACCSTAT_SETUP); case ThreadConnecting: return LLoadString(IDS_ACCSTAT_CONNECT); case ThreadTransfer: return LLoadString(IDS_ACCSTAT_TRANS); case ThreadWaiting: return LLoadString(IDS_ACCSTAT_WAIT); case ThreadDeleting: return LLoadString(IDS_ACCSTAT_DELETING); case ThreadDone: return LLoadString(IDS_ACCSTAT_FINISHED); case ThreadCancel: return LLoadString(IDS_ACCSTAT_CANCELLED); case ThreadError: return LLoadString(IDS_ERROR); } return "(none)"; } void Accountlet::OnThreadDone() { Thread.Reset(); if (DataStore && Root) Expanded(Root->Expanded()); DeleteObj(Root); } ScribeWnd *Accountlet::GetApp() { LAssert(Account->Parent != NULL); return Account->Parent; } LSocketI *Accountlet::CreateSocket(bool Sending, LCapabilityClient *Caps, bool RawLFCheck) { LSocketI *Socket = 0; LVariant File; LVariant Socks5Proxy; LVariant UseSocks; LVariant Format = (int)NET_LOG_NONE; GetApp()->GetOptions()->GetValue(OPT_LogFile, File); GetApp()->GetOptions()->GetValue(OPT_LogFormat, Format); int SslMode = UseSSL(); if (SslMode > 0) { Socket = new SslSocket(this, Caps, SslMode == SSL_DIRECT, RawLFCheck); } else if ( GetApp()->GetOptions()->GetValue(OPT_UseSocks, UseSocks) && UseSocks.CastInt32() && GetApp()->GetOptions()->GetValue(OPT_Socks5Server, Socks5Proxy) && ValidStr(Socks5Proxy.Str())) { LVariant Socks5UserName; LVariant Socks5Password; GetApp()->GetOptions()->GetValue(OPT_Socks5UserName, Socks5UserName); GetApp()->GetOptions()->GetValue(OPT_Socks5Password, Socks5Password); LUri Server(Socks5Proxy.Str()); Socket = new ILogSocks5Connection( Server.sHost, Server.Port?Server.Port:1080, Socks5UserName.Str(), Socks5Password.Str(), this); } else { Socket = new ILogConnection(this); } if (Socket) { Socket->SetTimeout(DEFAULT_SOCKET_TIMEOUT); if (File.Str()) { Socket->SetValue(OPT_LogFile, File); Socket->SetValue(OPT_LogFormat, Format); } Socket->SetCancel(Thread); } return Socket; } void Accountlet::Disconnect() { if (Thread) { Thread->Disconnect(); } else { if (MailStore) Account->Parent->OnMailStore(&MailStore, false); if (Root) { if (DataStore && Root) Expanded(Root->Expanded()); Root->Remove(); DeleteObj(Root); } DeleteObj(DataStore); } } void Accountlet::Kill() { if (Thread) { Thread->Kill(); } } bool Accountlet::Lock() { return Account->Parent->Lock(_FL); } void Accountlet::Unlock() { Account->Parent->Unlock(); } ssize_t Accountlet::Write(const void *buf, ssize_t size, int flags) { if (!Account->GetApp()) return 0; LColour col = SocketMsgTypeToColour((LSocketI::SocketMsgType)flags); I Lck = Lock(_FL); if (Lck) { bool Match = false; LArray &Log = Lck->d->Log; if (Log.Length() > 0) Match = Log.Last()->GetColour() == col; if (Match) { Log.Last()->Add((const char*)buf, size); } else { LogEntry *le = new LogEntry(col); if (le) { le->Add((const char*)buf, size); Log.Add(le); } } } return size; } void Accountlet::OnOnlineChange(bool Online) { if (DataStore && Root && Expanded()) { // uint64 s = LCurrentTime(); Root->Expanded(true); // uint64 e = LCurrentTime(); // LgiTrace("Expanded took: %i\n", (int)(e-s)); } } bool Accountlet::Connect(LView *p, bool quiet) { bool Status = false; I Lck = Lock(_FL); if (Lck) { Lck->d->Log.DeleteObjects(); Lck.Reset(); } if (Lock()) { Parent = p; Quiet = quiet; if (!Thread) { ReceiveAccountlet *Receive = NULL; if (IsReceive() && (Receive = &GetAccount()->Receive) && Receive->IsPersistant()) { LVariant Proto = GetAccount()->Receive.Protocol(); ScribeWnd *Wnd = GetAccount()->Parent; ScribeProtocol Protocol = ProtocolStrToEnum(Proto.Str()); #ifdef ImapSupport LString HostName = Server().Str(); if (!DataStore && Protocol == ProtocolImapFull && ValidStr(HostName)) { char Password[256] = ""; GPassword p; if (GetPassword(&p)) { p.Get(Password); } int OpenFlags = MakeOpenFlags(Account, false); MailProtocolProgress *Prog[2] = { &Group, &Item }; LAutoPtr Store; char StorePath[128]; if (OptionName(NULL, StorePath, sizeof(StorePath))) { Store.Reset(new ProtocolSettingStore(Wnd->GetOptions(), StorePath)); } DataStore = OpenImap(HostName, Receive->Port(), UserName().Str(), Password, OpenFlags, Account->GetApp(), GetAccount(), Prog, this, Id(), Store); if (DataStore) { DeleteObj(Root); if ((Root = new ScribeFolder)) { Root->App = Account->GetApp(); Root->SetLoadOnDemand(); LDataFolderI *r = DataStore->GetRoot(); if (r) Root->SetObject(r, false, _FL); Wnd->Tree->Insert(Root); } } } #endif #ifdef WIN32 if (!DataStore && Protocol == ProtocolMapi && ValidStr(Server().Str())) { char Password[256] = ""; GPassword p; if (GetPassword(&p)) p.Get(Password); DataStore = OpenMapiStore( Server().Str(), UserName().Str(), Password, Account->Receive.Id(), Account->GetApp()); if (DataStore) { LDataFolderI *r = DataStore->GetRoot(); if (r) { DeleteObj(Root); if ((Root = new ScribeFolder)) { Root->App = Account->GetApp(); Root->SetLoadOnDemand(); Root->SetObject(r, false,_FL); Wnd->Tree->Insert(Root); } } else LgiTrace("%s:%i - Failed to get data store root.\n", _FL); } } #endif } else { // Setup Enabled(false); I Lck = Lock(_FL); if (Lck) { Lck->d->Log.DeleteObjects(); Lck.Reset(); } auto StartThread = [&]() { if (Thread.Reset(new AccountletThread(this, 0))) { Status = true; Thread->Run(); Account->Parent->OnBeforeConnect(Account, IsReceive()); } }; // Do we need the password? LString Password; GPassword Psw; GetPassword(&Psw); Password = Psw.Get(); auto User = UserName(); if (User.Str() && !ValidStr(Password)) { auto RemoteHost = Server(); LString Msg; Msg.Printf(LLoadString(IDS_ASK_ACCOUNT_PASSWORD), User.Str(), RemoteHost.Str()); GetApp()->GetUserInput( Parent ? Parent : GetApp(), Msg, true, [&](auto Psw) { this->TempPsw = Psw; StartThread(); }); } else StartThread(); } } Unlock(); } return Status; } void Accountlet::IsCancelled(bool b) { if (Thread) Thread->Cancel(b); } bool Accountlet::IsCancelled() { return Thread ? Thread->IsCancelled() : false; } char *Accountlet::OptionName(const char *Opt, char *Dest, int DestLen) { int Idx = Account->GetIndex(); LAssert(Idx >= 0); if (Opt) { if (sprintf_s(Dest, DestLen, "Accounts.Account-%i.%s", Idx, Opt) < 0) return NULL; } else { if (sprintf_s(Dest, DestLen, "Accounts.Account-%i", Idx) < 0) return NULL; } return Dest; } bool Accountlet::GetPassword(GPassword *p) { bool Status = false; if (p && OptPassword && Lock()) { char Name[128]; OptionName(OptPassword, Name, sizeof(Name)); Status = p->Serialize(Account->GetApp()->GetOptions(), Name, false); Unlock(); } return Status; } void Accountlet::SetPassword(GPassword *p) { if (OptPassword && Lock()) { char Name[128]; OptionName(OptPassword, Name, sizeof(Name)); if (p) { p->Serialize(Account->GetApp()->GetOptions(), Name, true); } else { Account->GetApp()->GetOptions()->DeleteValue(Name); } Unlock(); } } bool Accountlet::IsCheckDialup() { LVariant CheckDialUp; char Name[128]; Account->GetApp()->GetOptions()->GetValue(OptionName(OPT_CheckForDialUp, Name, sizeof(Name)), CheckDialUp); return CheckDialUp.CastInt32() != 0; } //////////////////////////////////////////////////////////////////////////// AccountIdentity::AccountIdentity(ScribeAccount *a) : Accountlet(a) { } bool AccountIdentity::IsValid() { return ValidStr(Email().Str()) && ValidStr(Name().Str()); } void AccountIdentity::CreateMaps() { char Name[128]; Account->Map(OptionName(OPT_AccIdentName, Name, sizeof(Name)), IDC_NAME, GV_STRING); Account->Map(OptionName(OPT_AccIdentEmail, Name, sizeof(Name)), IDC_EMAIL, GV_STRING); Account->Map(OptionName(OPT_AccIdentReply, Name, sizeof(Name)), IDC_REPLY_TO, GV_STRING); Account->Map(OptionName(OPT_AccIdentTextSig, Name, sizeof(Name)), IDC_SIG, GV_STRING); Account->Map(OptionName(OPT_AccIdentHtmlSig, Name, sizeof(Name)), IDC_SIG_HTML, GV_STRING); } bool AccountIdentity::GetVariant(const char *Name, LVariant &Value, const char *Array) { /* case SdName: // Type: String case SdEmail: // Type: String case SdReply: // Type: String case SdSig: // Type: String case SdHtmlSig: // Type: String */ char n[128]; sprintf_s(n, sizeof(n), "Identity.%s", Name); return Accountlet::GetVariant(n, Value, Array); } bool AccountIdentity::SetVariant(const char *Name, LVariant &Value, const char *Array) { char n[128]; sprintf_s(n, sizeof(n), "Identity.%s", Name); return Accountlet::SetVariant(n, Value, Array); } //////////////////////////////////////////////////////////////////////////// SendAccountlet::SendAccountlet(ScribeAccount *a) : Accountlet(a) { SendItem = 0; // Options OptPassword = OPT_EncryptedSmtpPassword; } SendAccountlet::~SendAccountlet() { } bool SendAccountlet::GetVariant(const char *Name, LVariant &Value, const char *Array) { /* case SdServer: // Type: String case SdPort: // Type: Integer case SdDomain: // Type: String case SdName: // Type: String case SdAuth: // Type: Bool case SdAuthType: // Type: Integer case SdPrefCharset1: // Type: String case SdPrefCharset2: // Type: String case SdOnlySendThis: // Type: Bool case SdSSL: // Type: Integer */ char n[128]; sprintf_s(n, sizeof(n), "Send.%s", Name); return Accountlet::GetVariant(n, Value, Array); } bool SendAccountlet::SetVariant(const char *Name, LVariant &Value, const char *Array) { char n[128]; sprintf_s(n, sizeof(n), "Send.%s", Name); return Accountlet::SetVariant(n, Value, Array); } void SendAccountlet::CreateMaps() { // Fields char Name[256] = ""; Account->Map(OptionName(OPT_SmtpServer, Name, sizeof(Name)), IDC_SMTP_SERVER, GV_STRING); Account->Map(OptionName(OPT_SmtpPort, Name, sizeof(Name)), IDC_SMTP_PORT, GV_INT32); Account->Map(OptionName(OPT_SmtpDomain, Name, sizeof(Name)), IDC_SMTP_DOMAIN, GV_STRING); Account->Map(OptionName(OPT_SmtpName, Name, sizeof(Name)), IDC_SMTP_NAME, GV_STRING); Account->Map(OptionName(OPT_SmtpAuth, Name, sizeof(Name)), IDC_SMTP_AUTH, GV_BOOL); Account->Map(OptionName(OPT_SmtpAuthType, Name, sizeof(Name)), IDC_SEND_AUTH_TYPE, GV_INT32); Account->Map(OptionName(OPT_SendCharset1, Name, sizeof(Name)), IDC_SEND_CHARSET1, GV_STRING); Account->Map(OptionName(OPT_SendCharset2, Name, sizeof(Name)), IDC_SEND_CHARSET2, GV_STRING); Account->Map(OptionName(OPT_OnlySendThroughThis, Name, sizeof(Name)), IDC_ONLY_SEND_THIS, GV_BOOL); Account->Map(OptionName(OPT_SmtpSSL, Name, sizeof(Name)), IDC_SEND_SSL, GV_INT32); } ScribeAccountletStatusIcon SendAccountlet::GetStatusIcon() { return IsOnline() ? STATUS_ONLINE : STATUS_OFFLINE; } bool SendAccountlet::InitMenus() { // Menus LVariant n = Name(); LVariant s = Server(); LVariant u = UserName(); if (s.Str()) { char Name[256]; // Base name of menu if (n.Str()) { strcpy_s(Name, sizeof(Name), n.Str()); } else if (u.Str()) { sprintf_s(Name, sizeof(Name), "%s@%s", u.Str(), s.Str()); } else { strcpy_s(Name, sizeof(Name), s.Str()); } // Add send menuitem if (GetApp()->SendMenu) { LVariant Default; GetApp()->GetOptions()->GetValue(OPT_DefaultSendAccount, Default); if (Default.CastInt32() == Account->GetIndex()) { strcat(Name+strlen(Name), "\tCtrl-S"); } SendItem = GetApp()->SendMenu->AppendItem(Name, IDM_SEND_FROM+Account->GetIndex(), true); } return true; } return false; } void SendAccountlet::Enabled(bool b) { if (Account->GetIndex() == 0) { GetApp()->CmdSend.Enabled(b); } else if (SendItem) { SendItem->Enabled(b); } } void SendAccountlet::Main(AccountletThread *Thread) { bool Status = false; bool MissingConfig = false; LStringPipe Err; if (GetApp()) { LVariant v; GetApp()->GetOptions()->GetValue(OPT_DebugTrace, v); int DebugTrace = v.CastInt32(); LastOnline = LCurrentTime(); { MailSink *Sink; // LProtocolLogger Logger(GetApp(), M_SCRIBE_LOG_MSG, &Log); LVariant SendHotFolder = HotFolder(); if (SendHotFolder.Str() && LDirExists(SendHotFolder.Str())) { Client = Sink = new MailSendFolder(SendHotFolder.Str()); } else { Client = Sink = new MailSmtp; } if (Client) { LVariant s; if (GetApp()->GetOptions()->GetValue(OPT_ExtraHeaders, s)) { Client->ExtraOutgoingHeaders = s.Str(); } s = PrefCharset1(); if (s.Str()) { Sink->CharsetPrefs.Insert(NewStr(s.Str())); } s = PrefCharset2(); if (s.Str()) { Sink->CharsetPrefs.Insert(NewStr(s.Str())); } } if (Outbox.Length()) { int n = 0; for (unsigned Idx=0; IdxAction = MailUpload; t->Index = n++; t->Send = m; Thread->Files.Insert(t); } } if (DebugTrace) LgiTrace("Send(%i) got %i mail to send\n", Account->GetIndex(), Thread->Files.Length()); if (Sink && Thread->Files.Length()) { Sink->Logger = this; Sink->Transfer = &Item; if (Client) { LVariant _HotFld = HotFolder(); LVariant _Server = Server(); LVariant _Domain = Domain(); LVariant _UserName = UserName(); LString _Password; int Sent = 0; // int Total = Outbox.Length(); LVariant HideId = false; auto Opts = GetApp()->GetOptions(); Opts->GetValue(OPT_HideId, HideId); if (!HideId.CastInt32()) { Opts->GetValue(OPT_HideId, HideId); Client->ProgramName = GetFullAppName(!HideId.CastInt32()); } Client->SetSettingStore(Opts); if (ValidStr(_HotFld.Str()) || ValidStr(_Server.Str())) { bool Error = false; if (RequireAuthentication()) { GPassword p; if (GetPassword(&p)) _Password = p.Get(); } if (DebugTrace) LgiTrace("Send(%i) connecting to SMTP server\n", Account->GetIndex()); Thread->SetState(ThreadConnecting); int OpenFlags = MakeOpenFlags(Account, true); auto Params = GetOAuth2Params(_Server.Str(), MAGIC_MAIL); if (Params.IsValid()) { Sink->SetOAuthParams(Params); } if (!Sink->Open(CreateSocket(true, GetAccount(), true), _Server.Str(), _Domain.Str(), _UserName.Str(), _Password, Port(), OpenFlags)) { if (DebugTrace) LgiTrace("Send(%i) %s:%i\n", Account->GetIndex(), _FL); Err.Push(LLoadString(IDS_NO_CONNECTION_TO_SERVER)); Err.Push("\n"); } else { if (DebugTrace) LgiTrace("Send(%i) connected\n", Account->GetIndex()); Group.Value = 0; Group.Range = Thread->Files.Length(); Thread->SetState(ThreadTransfer); MailTransferEvent *e = NULL; for (auto FileIt = Thread->Files.begin(); FileIt != Thread->Files.end() && (e = dynamic_cast(*FileIt)) && !Thread->IsCancelled(); FileIt++, Group.Value++) { if (DebugTrace) LgiTrace("Send(%i) Item(%i) starting send\n", Account->GetIndex(), Group.Value); AddressDescriptor From; From.sAddr = e->Send->From; List ToLst; for (unsigned n=0; nSend->To.Length(); n++) { AddressDescriptor *ad = new AddressDescriptor; ad->sAddr = e->Send->To[n]; ToLst.Insert(ad); } MailProtocolError SendErr; LStringPipe *Out = Sink->SendStart(ToLst, &From, &SendErr); List *To = &ToLst; bool BadAddress = false; for (auto a: *To) { if (!a->Status) { if (a->sAddr.Find(",") >= 0) { auto t = a->sAddr.SplitDelimit(","); for (unsigned i=0; iPrint().Get()); } BadAddress = true; } } if (BadAddress) { LString m; m.Printf(LLoadString(Sink->ErrMsgId, Sink->ErrMsgFmt), Sink->ErrMsgParam.Get()); Err.Push(m.Get()); } if (Out) { LStringPipe InetHeaders; ssize_t Len = e->Send->Rfc822.Length(); ssize_t Written = 0; Item.Range = Len; Item.Start = LCurrentTime(); for (ssize_t Pos = 0; Pos < Len; ) { ssize_t Remaining = Len - Pos; ssize_t Block = MIN(Remaining, 4 << 10); ssize_t Wr = Out->Write(e->Send->Rfc822.Get() + Pos, Block); if (Wr <= 0) break; Pos += Wr; Written += Wr; Item.Value = Pos; } Item.Start = 0; if (Written == Len) { if (Sink->SendEnd(Out)) { if (DebugTrace) LgiTrace("Send(%i) Item(%i) success\n", Account->GetIndex(), Group.Value); Status = true; Sent++; e->OutgoingHeaders = InetHeaders.NewStr(); if (e->Send->References) { GetApp()->PostEvent(M_SCRIBE_SET_MSG_FLAG, (LMessage::Param)NewStr(e->Send->References), MAIL_REPLIED); } if (e->Send->FwdMsgId) { GetApp()->PostEvent(M_SCRIBE_SET_MSG_FLAG, (LMessage::Param)NewStr(e->Send->FwdMsgId), MAIL_FORWARDED); } if (e->Send->BounceMsgId) { GetApp()->PostEvent(M_SCRIBE_SET_MSG_FLAG, (LMessage::Param)NewStr(e->Send->BounceMsgId), MAIL_BOUNCED); } e->Account = this; e->Status = MailReceivedWaiting; GetApp()->OnMailTransferEvent(e); } else { Write("SendEnd failed.\n", -1, LSocketI::SocketMsgError); Error = true; break; } } else { Write("Encoding failed.\n", -1, LSocketI::SocketMsgError); Error = true; break; } } else { LString s; s.Printf("SendStart failed: %i, %s\n", SendErr.Code, SendErr.ErrMsg.Get()); Write(s, -1, LSocketI::SocketMsgError); Error = true; break; } } if (DebugTrace) LgiTrace("Send(%i) closing, Error=%i\n", Account->GetIndex(), Error); if (Error) { // we can't call Client->Close() on error because it // sends a quit command and expects a reply. Under // error conditions that can fail because the server // might not be in a state to handle that request. // So just close the socket and call it a day. if (DebugTrace) LgiTrace("Send(%i) %s:%i\n", Account->GetIndex(), _FL); Sink->CloseSocket(); } else { // Normal quit if (DebugTrace) LgiTrace("Send(%i) %s:%i\n", Account->GetIndex(), _FL); Sink->Close(); } if (DebugTrace) LgiTrace("Send(%i) %s:%i\n", Account->GetIndex(), _FL); Group.Value = 0; Group.Range = 0; Item.Value = 0; Item.Range = 0; Status &= WaitForTransfers(Thread->Files); } if (DebugTrace) LgiTrace("Send(%i) %s:%i, status=%i\n", Account->GetIndex(), _FL, Status); if (!Status) { if (DebugTrace) LgiTrace("Send(%i) %s:%i, Client=%p\n", Account->GetIndex(), _FL, Client); if (ValidStr(Client->ErrMsgFmt)) { LString m; m.Printf(LLoadString(Sink->ErrMsgId, Sink->ErrMsgFmt), Sink->ErrMsgParam.Get()); Err.Push("\n"); Err.Push(m.Get()); Err.Push("\n"); } } } else { MissingConfig = true; Err.Push(LLoadString(IDS_NO_SMTP_SERVER_SETTINGS)); Err.Push("\n"); } } if (DebugTrace) LgiTrace("Send(%i) %s:%i, Thread=%p\n", Account->GetIndex(), _FL, Thread); if (DebugTrace) LgiTrace("Send(%i) %s:%i\n", Account->GetIndex(), _FL); } Outbox.DeleteObjects(); } DeleteObj(Sink); } if (DebugTrace) LgiTrace("Send(%i) exit\n", Account->GetIndex()); } else { Thread->SetState(ThreadError); } char *e = 0; if (MissingConfig) { Err.Push(LLoadString(IDS_ASK_ABOUT_OPTIONS)); Err.Push("\n"); e = Err.NewStr(); GetApp()->PostEvent(M_SCRIBE_MSG, (LMessage::Param)e, 1); } else if ((e = Err.NewStr())) { GetApp()->PostEvent(M_SCRIBE_MSG, (LMessage::Param)e); } } //////////////////////////////////////////////////////////////////////////// ReceiveAccountlet::ReceiveAccountlet(ScribeAccount *a) : Accountlet(a) { // Init SecondsTillOnline = -1; LVariant Ct = CheckTimeout(); if (Ct.Str()) { SecondsTillOnline = GetCheckTimeout(); } ReceiveItem = 0; PreviewItem = 0; Items = 0; IdTemp = 0; char Key[128]; OptionName("Messages", Key, sizeof(Key)); Msgs.Reset(new MsgList(a->GetApp()->GetOptions(), Key)); OptionName("Spam", Key, sizeof(Key)); Spam.Reset(new MsgList(a->GetApp()->GetOptions(), Key)); // Upgrade props LVariant OldType = -1; char Buf[128]; LOptionsFile *Options = GetApp()->GetOptions(); if (Options->GetValue(OptionName(OPT_Pop3Type, Buf, sizeof(Buf)), OldType)) { // Old Account types: #define MAIL_SOURCE_POP3 0 #define MAIL_SOURCE_IMAP4 1 #define MAIL_SOURCE_MAPI 2 #define MAIL_SOURCE_SCP 3 #define MAIL_SOURCE_HTTP 4 switch (OldType.CastInt32()) { case MAIL_SOURCE_POP3: Protocol(PROTOCOL_POP3); break; case MAIL_SOURCE_IMAP4: Protocol(PROTOCOL_IMAP4_FETCH); break; case MAIL_SOURCE_SCP: Protocol(PROTOCOL_CALENDAR); break; case MAIL_SOURCE_HTTP: Protocol(PROTOCOL_POP_OVER_HTTP); break; case MAIL_SOURCE_MAPI: Protocol(PROTOCOL_MAPI); break; } Options->DeleteValue(Buf); } char Name[256] = ""; LVariant Auto; if (!Options->GetValue(OptionName(OPT_Pop3AutoReceive, Name, sizeof(Name)), Auto)) { LVariant s; Options->GetValue(OptionName(OPT_Pop3CheckEvery, Name, sizeof(Name)), s); if (s.Str()) { int Timeout = atoi(s.Str()); Options->SetValue(OptionName(OPT_Pop3AutoReceive, Name, sizeof(Name)), s = (Timeout > 0)); } } // Options OptPassword = OPT_EncryptedPop3Password; } ReceiveAccountlet::~ReceiveAccountlet() { } bool ReceiveAccountlet::GetVariant(const char *Name, LVariant &Value, const char *Array) { char n[128]; sprintf_s(n, sizeof(n), "Receive.%s", Name); return Accountlet::GetVariant(n, Value, Array); } bool ReceiveAccountlet::SetVariant(const char *Name, LVariant &Value, const char *Array) { char n[128]; sprintf_s(n, sizeof(n), "Receive.%s", Name); return Accountlet::SetVariant(n, Value, Array); } ScribeAccountletStatusIcon ReceiveAccountlet::GetStatusIcon() { if (DataStore) { int64 s = DataStore->GetInt(FIELD_STORE_STATUS); if (s >= STATUS_OFFLINE && s < STATUS_MAX) return (ScribeAccountletStatusIcon)s; } if (IsOnline()) return STATUS_ONLINE; if (!GetStatus()) return STATUS_ERROR; if (AutoReceive()) return STATUS_WAIT; return STATUS_OFFLINE; } void ReceiveAccountlet::CreateMaps() { char Name[256] = ""; // Fields Account->Map(OptionName(OPT_Pop3Protocol, Name, sizeof(Name)), IDC_REC_TYPE, GV_STRING); Account->Map(OptionName(OPT_Pop3Server, Name, sizeof(Name)), IDC_REC_SERVER, GV_STRING); Account->Map(OptionName(OPT_Pop3Port, Name, sizeof(Name)), IDC_REC_PORT, GV_INT32); Account->Map(OptionName(OPT_Pop3Name, Name, sizeof(Name)), IDC_REC_NAME, GV_STRING); Account->Map(OptionName(OPT_Pop3AutoReceive, Name, sizeof(Name)), IDC_CHECK_EVERY, GV_BOOL); Account->Map(OptionName(OPT_Pop3CheckEvery, Name, sizeof(Name)), IDC_REC_CHECK, GV_STRING); Account->Map(OptionName(OPT_Pop3LeaveOnServer, Name, sizeof(Name)), IDC_POP3_LEAVE, GV_BOOL); Account->Map(OptionName(OPT_DeleteAfter, Name, sizeof(Name)), IDC_DELETE_AFTER, GV_BOOL); Account->Map(OptionName(OPT_DeleteDays, Name, sizeof(Name)), IDC_DELETE_DAYS, GV_INT32); Account->Map(OptionName(OPT_DeleteIfLarger, Name, sizeof(Name)), IDC_DELETE_LARGER, GV_BOOL); Account->Map(OptionName(OPT_DeleteIfLargerSize, Name, sizeof(Name)), IDC_DELETE_SIZE, GV_INT32); Account->Map(OptionName(OPT_Pop3Folder, Name, sizeof(Name)), IDC_FOLDER, GV_STRING); Account->Map(OptionName(OPT_MaxEmailSize, Name, sizeof(Name)), IDC_MAX_SIZE, GV_INT32); Account->Map(OptionName(OPT_Receive8BitCs, Name, sizeof(Name)), IDC_REC_8BIT_CS, GV_STRING); Account->Map(OptionName(OPT_ReceiveAsciiCs, Name, sizeof(Name)), IDC_REC_ASCII_CP, GV_STRING); Account->Map(OptionName(OPT_ReceiveAuthType, Name, sizeof(Name)), IDC_RECEIVE_AUTH_TYPE, GV_INT32); Account->Map(OptionName(OPT_Pop3SSL, Name, sizeof(Name)), IDC_RECEIVE_SSL, GV_INT32); Account->Map(OptionName(OPT_ReceiveSecAuth, Name, sizeof(Name)), IDC_SEC_AUTH, GV_INT32); } int ReceiveAccountlet::GetCheckTimeout() { int Sec = -1; LVariant Timeout = CheckTimeout(); if (Timeout.Str()) { - LToken t(Timeout.Str(), ":"); + auto t = Timeout.LStr().SplitDelimit(":"); if (t.Length() == 2) { Sec = (atoi(t[0]) * 60) + atoi(t[1]); } else { Sec = atoi(Timeout.Str()) * 60; } } return Sec; } bool ReceiveAccountlet::InitMenus() { // Menus LVariant n = Name(); LVariant s = Server(); LVariant u = UserName(); if (IsConfigured()) { char Name[256]; // Base name of menu if (n.Str()) strcpy_s(Name, sizeof(Name), n.Str()); else if (u.Str() && s.Str()) sprintf_s(Name, sizeof(Name), "%s@%s", u.Str(), s.Str()); else if (u.Str()) strcpy_s(Name, sizeof(Name), u.Str()); else if (s.Str()) strcpy_s(Name, sizeof(Name), s.Str()); else strcpy_s(Name, sizeof(Name), "(untitled)"); // Add preview menuitem if (GetApp()->PreviewMenu) { PreviewItem = GetApp()->PreviewMenu->AppendItem(Name, IDM_PREVIEW_FROM+Account->GetIndex(), !Disabled()); } // Add shortcut if (Account->GetIndex() < 9) { size_t Len = strlen(Name); sprintf_s(Name+Len, sizeof(Name)-Len, "\tCtrl+%i", Account->GetIndex()+1); } // Add receive menuitem if (GetApp()->ReceiveMenu) { ReceiveItem = GetApp()->ReceiveMenu->AppendItem(Name, IDM_RECEIVE_FROM+Account->GetIndex(), !Disabled()); } return true; } return false; } bool ReceiveAccountlet::RemoveFromSpamIds(const char *Id) { if (!Id || !Spam) return false; if (!Lock()) return false; Spam->Delete(Id); Spam->Dirty = true; Unlock(); return true; } void ReceiveAccountlet::DeleteAsSpam(const char *Id) { if (HasMsg(Id)) { RemoveMsg(Id); if (Lock()) { if (Spam) { if (!Spam->Find(Id)) { Spam->Add(Id); Spam->Dirty = true; } } Unlock(); } } } bool ReceiveAccountlet::IsSpamId(const char *Id, bool Delete) { bool Status = false; if (ValidStr(Id)) { if (Lock()) { if (Spam) { Status = Spam->Find(Id); } Unlock(); } } return Status; } bool ReceiveAccountlet::SetActions(LArray *a) { bool Status = false; if (Lock()) { if (!IsOnline()) { if (a) Actions = *a; else Actions.Length(0); Status = true; } else LAssert(!"Trying to set actions when online is forbidden."); Unlock(); } return Status; } bool ReceiveAccountlet::SetItems(LList *l) { bool Status = false; if (Lock()) { Items = l; Status = true; Unlock(); } return Status; } void ReceiveAccountlet::Enabled(bool b) { if (Account->GetIndex() == 0) { GetApp()->CmdReceive.Enabled(b); GetApp()->CmdPreview.Enabled(b); } if (ReceiveItem) { ReceiveItem->Enabled(b); } if (PreviewItem) { PreviewItem->Enabled(b); } } int FilterCompare(Filter *a, Filter *b, NativeInt Data) { return a->GetIndex() - b->GetIndex(); } bool ReceiveAccountlet::IsPersistant() { LVariant Proto = Protocol(); if (Proto.Str()) { #ifdef ImapSupport if (_stricmp(Proto.Str(), PROTOCOL_IMAP4) == 0) return true; #endif #ifdef WIN32 if (!_stricmp(Proto.Str(), PROTOCOL_MAPI)) return true; #endif } return false; } int64 ConvertRelativeSize(char *Size, int64 Total) { int64 Status = -1; if (Size) { double f = atof(Size); if (strchr(Size, '%')) { Status = (int64)(Total * f) / 100; } else if (stristr(Size, "KB")) { Status = (int64)(f * 1024); } else if (stristr(Size, "MB")) { Status = (int64)(f * 1024 * 1024); } else if (stristr(Size, "GB")) { Status = (int64)(f * 1024 * 1024 * 1024); } else { Status = (int64)f; } } return Status; } struct MailReceiveParams { ScribeWnd *App; AccountletThread *Thread; uint64 MaxSize; uint64 NoAttachLimit; uint64 NoDownloadLimit; char *KitFileName; int DownloadLines; MailReceiveParams() { App = 0; Thread = 0; MaxSize = 0; NoAttachLimit = 0; NoDownloadLimit = 0; KitFileName = 0; DownloadLines = 0; } }; bool AfterReceived(MailTransaction *Trans, void *Data) { if (Trans->Oversize) { } else if (Trans->Status) { MailReceiveParams *p = (MailReceiveParams*) Data; if (!p) return false; MailTransferEvent *t = p->Thread->Files[Trans->Index]; if (!t) return false; Trans->Flags |= MAIL_POSTED_TO_GUI; if (t->Rfc822Msg) { // t->Data->Parse(); } // Set the event status t->Status = MailReceivedWaiting; t->Account = p->Thread->GetAccountlet(); // Ask the gui thread to load the mail in p->App->OnMailTransferEvent(t); } return true; } MailSrcStatus ReceiveCallback(MailTransaction *Trans, uint64 Size, int *TopLines, void *Data) { MailReceiveParams *Params = (MailReceiveParams*) Data; if (Params->KitFileName) { uint64 Free; if (LGetDriveInfo(Params->KitFileName, &Free)) { if (Free <= Params->NoDownloadLimit) { return DownloadNone; } else if (Free <= Params->NoAttachLimit) { if (TopLines && Params->DownloadLines > 0) { *TopLines = Params->DownloadLines; } return DownloadTop; } } } if (Params->MaxSize) { if (Size > Params->MaxSize) { if (!TestFlag(Trans->Flags, MAIL_EXPLICIT)) { return DownloadNone; } } } return DownloadAll; } bool ReceiveAccountlet::OnIdle() { return DataStore ? DataStore->OnIdle() : false; } void ReceiveAccountlet::Main(AccountletThread *Thread) { bool Status = false; LVariant v; LastOnline = LCurrentTime(); #define TimeDelta() ((int) (LCurrentTime() - LastOnline)) SecondsTillOnline = -1; int DebugTrace = false; GetApp()->GetOptions()->GetValue(OPT_DebugTrace, v); DebugTrace = v.CastInt32(); if (DebugTrace) LgiTrace("Receive(%i) starting, %i\n", Account->GetIndex(), TimeDelta()); auto RemoteHost = Server(); auto RemotePort = Port(); auto User = UserName(); // int LeaveCopy = LeaveOnServer(); int DeleteIfLargerThan = DeleteLarger() ? DeleteSize() << 10 : 0; MailReceiveParams Params; Params.App = GetApp(); Params.Thread = Thread; Params.MaxSize = DownloadLimit() << 10; auto MailSourceType = ProtocolStrToEnum(Protocol().Str()); auto Ms = GetApp()->GetDefaultMailStore(); LString Password; GPassword Psw; GetPassword(&Psw); Password = Psw.Get(); MailSource *Source = 0; LVariant RecHotFolder = HotFolder(); if (RecHotFolder.Str() && LDirExists(RecHotFolder.Str())) { Client = Source = new MailReceiveFolder(RecHotFolder.Str()); } else { switch (MailSourceType) { case ProtocolPop3: { Client = Source = new MailPop3; break; } case ProtocolImapFetch: { Client = Source = new MailIMap; break; } case ProtocolPopOverHttp: { Client = Source = new MailPhp; break; } default: { LAssert(!"Unsupported protocol."); return; } } } if (DebugTrace) LgiTrace("Receive(%i) protocol=%i client=%p, time=%i\n", Account->GetIndex(), MailSourceType, Client, TimeDelta()); if (Source) { auto HttpProxy = GetApp()->GetHttpProxy(); if (HttpProxy) { LUri Host(HttpProxy); if (Host.sHost) Source->SetProxy(Host.sHost, Host.Port?Host.Port:80); } // Setup logging Source->Logger = this; Source->Items = &Group; Source->Transfer = &Item; auto OpenFlags = MakeOpenFlags(Account, false); auto NeedsPassword = !ValidStr(Password) && ValidStr(User.Str()); if (NeedsPassword) { if (TempPsw) Password = TempPsw; else if (!SecureAuth()) LAssert(!"Need to ask user for password BEFORE we're in the worker thread."); } if (DebugTrace) LgiTrace("Receive(%i) opening connection..., time=%i\n", Account->GetIndex(), TimeDelta()); Thread->SetState(ThreadConnecting); LHashTbl,bool> Uids; ReceiveAccountlet *Receive = dynamic_cast(Thread->Acc); if (!RecHotFolder.Str() && !SecureAuth() && !ValidStr(Password)) { Status = true; } else if (!Source->Open( CreateSocket(false, GetAccount(), false), RemoteHost.Str(), RemotePort, User.Str(), Password, SettingStore, OpenFlags)) { Thread->SetState(ThreadError); } else { if (DebugTrace) LgiTrace("Receive(%i) connected, time=%i\n", Account->GetIndex(), TimeDelta()); SecondsTillOnline = -1; // Get all the messages.. Thread->SetState(ThreadTransfer); auto Msgs = Source->GetMessages(); if (Msgs) { bool LeaveOnServer = Receive->LeaveOnServer() != 0; bool DeleteAfter = Receive->DeleteAfter() != 0; int DeleteDays = Receive->DeleteDays(); bool GetUids = LeaveOnServer; bool HasGetHeaders = false; if (DeleteDays < 1) { Receive->DeleteDays(DeleteDays = 1); } for (int i=0; iRfc822Msg.Reset(new LTempStream(ScribeTempPath())); t->Index = i; if (Actions.Length()) { t->Action = (unsigned)i < Actions.Length() ? Actions[i] : MailNoop; t->Explicit = true; } else if (Receive->Items) { t->Action = MailHeaders; GetUids = true; } else { t->Action = LeaveOnServer ? MailDownload : MailDownloadAndDelete; } switch (t->Action) { default: break; case MailHeaders: { HasGetHeaders = true; // fall thru } case MailDownloadAndDelete: case MailDownload: case MailUpload: { Group.Range++; break; } } Thread->Files.Insert(t); } } if (DebugTrace) LgiTrace("Receive(%i) got %i actions, time=%i\n", Account->GetIndex(), Thread->Files.Length(), TimeDelta()); if (HasGetHeaders || DeleteIfLargerThan) { LArray Sizes; if (Source->GetSizes(Sizes)) { unsigned i = 0; for (auto t: Thread->Files) { if (i >= Sizes.Length()) break; t->Size = Sizes[i]; if (t->Action == MailHeaders) { t->Msg = new AccountMessage(Account); if (t->Msg) { t->Msg->Size = t->Size; } } i++; } } } // Getting the UID's of the messages on the server if (GetUids || Receive->Msgs->Length() > 0) { if (DebugTrace) LgiTrace("Receive(%i) getting UID's, time=%i\n", Account->GetIndex(), TimeDelta()); LString::Array UidLst; Source->GetUidList(UidLst); for (auto u: UidLst) Uids.Add(u, true); // Assign all the ID strings to the transfer events LDateTime Now; Now.SetNow(); for (auto t: Thread->Files) { auto k = UidLst[0]; if (k) { UidLst.DeleteAt(0, true); t->Uid = k; if (DebugTrace) LgiTrace("\tUid[%i]='%s' (time=%i)\n", t->Index, t->Uid.Get(), TimeDelta()); } else break; if (!t->Explicit && DeleteAfter) { // Check how long the message has been on the server. LDateTime MsgDate; if (Receive->Msgs->GetDate(t->Uid, &MsgDate)) { LDateTime Days = Now - MsgDate; if (Days.Day() > DeleteDays) { if (t->Action == MailDownload) t->Action = MailDownloadAndDelete; } } } } } if (DebugTrace) LgiTrace("Receive(%i) starting main action loop, time=%i\n", Account->GetIndex(), TimeDelta()); Group.Start = LCurrentTime(); bool Error = false; LArray Trans; char NotLoaded[256]; sprintf_s( NotLoaded, sizeof(NotLoaded), "%s: %s", LLoadString(FIELD_SUBJECT), LLoadString(IDS_NOT_LOADED)); for (auto it = Thread->Files.rbegin(); it != Thread->Files.end() && !Thread->IsCancelled(); it--) { MailTransferEvent *t = *it; if (!t) continue; bool Ok = false; switch (t->Action) { case MailDownload: case MailDownloadAndDelete: { if (!t->Explicit && t->Uid && Receive->Msgs->Find(t->Uid)) { // Already got it... // if (DebugTrace) LgiTrace("Receive(%i) Item(%i) Already got msg\n", Account->GetIndex(), t->Index); Status = true; Group.Value++; if (DeleteIfLargerThan > 0 && t->Size > 0 && t->Size > DeleteIfLargerThan) { t->Action = MailDelete; } continue; } else { if (t->Uid && Receive->IsSpamId(t->Uid)) { // Delete the spam t->Action = MailDelete; Status = true; Group.Value++; continue; } else { // Download it MailTransaction *Get = new MailTransaction; if (Get) { if (t->Explicit) Get->Flags |= MAIL_EXPLICIT; Get->Index = t->Index; Get->Stream = t->Rfc822Msg; Trans.Add(Get); continue; } } Group.Value++; } break; } case MailHeaders: { if (DebugTrace) LgiTrace("Receive(%i) Item(%i) Getting headers..., time=%i\n", Account->GetIndex(), t->Index, TimeDelta()); auto Headers = Source->GetHeaders(t->Index); if (DebugTrace) LgiTrace("Receive(%i) Item(%i) headers=%p, time=%i\n", Account->GetIndex(), t->Index, Headers.Get(), TimeDelta()); if (!Headers) Headers = NotLoaded; if (Headers) { if (!t->Msg) t->Msg = new AccountMessage(Account); if (t->Msg) { bool Attachments = false; LAutoString ContentType(InetGetHeaderField(Headers, "Content-Type")); if (ContentType) { Attachments = stristr(ContentType, "multipart/mixed") != NULL; } t->Msg->Index = t->Index; t->Msg->From = DecodeRfc2047(InetGetHeaderField(Headers, "From")); t->Msg->Subject = DecodeRfc2047(InetGetHeaderField(Headers, "Subject")); t->Msg->ServerUid = NewStr(t->Uid); LAutoString d(InetGetHeaderField(Headers, "Date")); if (d) t->Msg->Date.Decode(d); t->Msg->Attachments = Attachments; if (IsSpamId(t->Uid)) { t->Msg->Download->Value(false); t->Msg->Delete->Value(true); t->Msg->New = false; } else { t->Msg->New = !HasMsg(t->Uid); } Ok = true; } } Group.Value++; break; } default: { continue; } } if (Ok) { Status = true; t->Account = this; t->Status = MailReceivedWaiting; GetApp()->OnMailTransferEvent(t); } else { if (DebugTrace) LgiTrace("Receive(%i) Item(%i) Error, time=%i\n", Account->GetIndex(), t->Index, TimeDelta()); Error = true; break; } } if (Trans.Length() > 0) { MailCallbacks Callbacks; ZeroObj(Callbacks); Callbacks.CallbackData = &Params; Callbacks.OnSrc = ReceiveCallback; Callbacks.OnReceive = AfterReceived; Error = !Source->Receive(Trans, &Callbacks); for (unsigned i=0; iFiles[Tran->Index]; if (t) { LgiTrace("%s:%i - Trans[%i]: No 't' ptr for idx=%i files.len=%i.\n", _FL, i, Tran->Index, (int)Thread->Files.Length()); } else { if (Tran->Oversize) { // Ignore t->Action = MailNoop; t->Status = MailReceivedOk; Status = true; } else if (Tran->Status) { if (t->Status == MailReceivedNone) { if (DebugTrace) LgiTrace("Receive(%i) Item(%i) Posting WM_SCRIBE_THREAD_ITEM, time=%i\n", Account->GetIndex(), t->Index, TimeDelta()); Status = true; if (!TestFlag(Tran->Flags, MAIL_POSTED_TO_GUI)) { // Ask the gui thread to load the mail in t->Status = MailReceivedWaiting; GetApp()->OnMailTransferEvent(t); } } else { Status = true; } } else { LgiTrace("%s:%i - Error: Bad Status on download %i of %i.\n", _FL, i, Trans.Length()); // Don't delete a mail that failed to download t->Action = MailNoop; } } } Trans.DeleteObjects(); } // Done... wait for main thread to finish processing if (DebugTrace) LgiTrace("Receive(%i) Waiting for main thread, time=%i\n", Account->GetIndex(), TimeDelta()); Thread->SetState(ThreadWaiting); // Wait for the main thread to finish with all the items we sent over if (!WaitForTransfers(Thread->Files)) Error = true; if (Error) { LgiTrace("%s:%i - Error receiving mail.\n", _FL); } else { // Do delete's if (DebugTrace) LgiTrace("Receive(%i) Delete phase, time=%i\n", Account->GetIndex(), TimeDelta()); Group.Empty(); { for (auto d: Thread->Files) { if (d->Action == MailDelete || d->Action == MailDownloadAndDelete) { Group.Range++; } } } Group.Start = LCurrentTime(); Thread->SetState(ThreadDeleting); for (auto It = Thread->Files.rbegin(); It != Thread->Files.end() && Thread->GetState() == ThreadDeleting; It--) { MailTransferEvent *d = *It; if (d->Action == MailDelete || d->Action == MailDownloadAndDelete) { if (DebugTrace) LgiTrace("Receive(%i) Delete(%i) Deleting, time=%i\n", Account->GetIndex(), d->Index, TimeDelta()); if (Source->Delete(d->Index)) { Status = true; if (d->Uid) { Uids.Delete(d->Uid); } } Group.Value++; } } } WaitForTransfers(Thread->Files); Thread->Files.DeleteObjects(); Group.Empty(); } else { Receive->RemoveAllMsgs(); Status = true; } // Clear out the UID's if (Uids.Length() && Receive->Msgs) { // ssize_t UidsLen = Uids.Length(); // ssize_t AllMsgIdsLen = Receive->Msgs->Length(); auto MsgKeys = Receive->Msgs->CopyKeys(); for (auto &k : MsgKeys) { if (!Uids.Find(k)) RemoveMsg(k); } if (Spam) { auto SpamKeys = Spam->CopyKeys(); for (auto &s : SpamKeys) { if (!Uids.Find(s)) Spam->Delete(s); } } } if (DebugTrace) LgiTrace("Receive(%i) Closing the connection, time=%i\n", Account->GetIndex(), TimeDelta()); // Close the connection. Source->Close(); } DeleteObj(Source); } else { Thread->SetState(ThreadError); } // Clean up, notify the app ConnectionStatus = Status; Actions.Length(0); if (DebugTrace) LgiTrace("Receive(%i) Exit, time=%i\n", Account->GetIndex(), TimeDelta()); } void ReceiveAccountlet::OnPulse(char *s, int s_len) { // this is called every second if (Lock()) { LVariant Offline; bool On = IsOnline(); LString Serv = Server().Str(); if (On) { strcat(s, "Online"); SecondsTillOnline = -1; if (Thread && Thread->GetState() == LThread::THREAD_EXITED) { #ifdef _DEBUG LgiTrace("Caught exited thread state, Accountlet[%i]=%p, Thread=%p, &Thread=%p\n", GetAccount()->GetIndex(), this, Thread.Get(), &Thread); #endif Thread.Reset(); } } else if ((GetApp()->GetOptions()->GetValue(OPT_WorkOffline, Offline) && Offline.CastInt32()) || Disabled() > 0) { strcat(s, "Offline"); SecondsTillOnline = -1; } else { if (AutoReceive()) { if (SecondsTillOnline < 0) { int To = GetCheckTimeout(); SecondsTillOnline = To > 0 ? To : 10 * 60; } } else { SecondsTillOnline = -1; } if (SecondsTillOnline == 0) { SecondsTillOnline = -1; if ((!IsCheckDialup() || HaveNetConnection()) && !Offline.CastInt32()) { Connect(0, false); } } else if (SecondsTillOnline > 0) { SecondsTillOnline--; if (CheckTimeout().Str()) { sprintf_s(s, s_len, "%i:%2.2i", SecondsTillOnline/60, SecondsTillOnline%60); } } } Unlock(); } } bool ReceiveAccountlet::HasMsg(const char *Id) { if (!Id || !Msgs || !Lock()) return false; auto Status = Msgs->Find(Id); Unlock(); return Status; } void ReceiveAccountlet::AddMsg(const char *Id) { if (!Msgs || !Lock()) return; if (!Msgs->Find(Id)) Msgs->Add(Id); Unlock(); } void ReceiveAccountlet::RemoveMsg(const char *Id) { if (!Id || !Msgs || !Lock()) return; Msgs->Delete(Id); Unlock(); } void ReceiveAccountlet::RemoveAllMsgs() { if (!Msgs || !Lock()) return; Msgs->Empty(); Unlock(); } int ReceiveAccountlet::GetMsgs() { int Status = 0; if (Lock()) { if (Msgs) { Status = Msgs->Length(); } Unlock(); } return Status; } ////////////////////////////////////////////////////////////////////////////////// #define OPT_MsgDate "Date" MsgList::MsgList(LOptionsFile *opts, char *tag) { Opts = opts; Tag = tag; Loaded = false; Dirty = false; } MsgList::~MsgList() { } bool MsgList::Load() { if (Opts && Tag && !Loaded) { LXmlTag *Msg = Opts->LockTag(Tag, _FL); if (!Msg) { Opts->CreateTag(Tag); Msg = Opts->LockTag(Tag, _FL); } if (Msg) { for (auto t: Msg->Children) { if (!t->GetAttr(OPT_MsgDate)) { LDateTime Now; char n[64]; Now.SetNow(); Now.Get(n, sizeof(n)); t->SetAttr(OPT_MsgDate, n); } Parent::Add(t->GetContent(), t); } Opts->Unlock(); } Loaded = true; } return Loaded; } LXmlTag *MsgList::LockId(const char *id, const char *file, int line) { if (Load()) { LXmlTag *r = Opts->LockTag(Tag, file, line); if (r) { LXmlTag *m = (LXmlTag*) Parent::Find(id); if (m) { // deliberately leave the options locked. return m; } Opts->Unlock(); } } return 0; } void MsgList::Unlock() { Opts->Unlock(); } bool MsgList::Add(const char *id) { bool Status = false; LXmlTag *r = Opts->LockTag(Tag, _FL); if (r) { if (!Parent::Find(id)) { LXmlTag *t = new LXmlTag("Message"); if (t) { LDateTime Now; char n[64]; Now.SetNow(); Now.Get(n, sizeof(n)); t->SetContent(id); t->SetAttr(OPT_MsgDate, n); r->InsertTag(t); Status = Parent::Add(id, t); } } Unlock(); } return Status; } bool MsgList::Delete(const char *id) { bool Status = false; LXmlTag *r = Opts->LockTag(Tag, _FL); if (r) { LXmlTag *t = Parent::Find(id); if (t) { // LgiTrace("DelMsg '%s' = %p\n", id, t); Parent::Delete(id); t->RemoveTag(); DeleteObj(t); Status = true; } Opts->Unlock(); } return Status; } int MsgList::Length() { if (Load()) { return (int)Parent::Length(); } return 0; } bool MsgList::Find(const char *id) { if (Load()) { return Parent::Find(id) != 0; } return 0; } LString::Array MsgList::CopyKeys() { LString::Array a(Length()); for (auto i : *this) { a.Add(i.key); } return a; } void MsgList::Empty() { if (!Load()) return; LXmlTag *Msg = Opts->LockTag(Tag, _FL); if (!Msg) return; LXmlTag *t; while ( Msg->Children.Length() && (t = Msg->Children[0])) { DeleteObj(t); } Opts->Unlock(); Parent::Empty(); } bool MsgList::SetDate(char *id, LDateTime *dt) { bool Status = false; if (id && dt) { LXmlTag *t = LockId(id, _FL); if (t) { char s[64]; LAssert(dt->IsValid()); // Force the date to save in a guessable format. // yyyy/m/d is always easy to sniff. dt->SetFormat(GDTF_YEAR_MONTH_DAY); dt->Get(s, sizeof(s)); t->SetAttr(OPT_MsgDate, s); Status = true; Unlock(); } } return Status; } bool MsgList::GetDate(char *id, LDateTime *dt) { if (!id || !dt) return false; LXmlTag *t = LockId(id, _FL); if (!t) return false; char *s = t->GetAttr(OPT_MsgDate); if (s) { // This is where I fucked up... previous versions of the software // saved the date in whatever the currently selected date format. // Which could've been anything... and the user can change that. // So if they do change it, the saved dates are now in the wrong // format to load back in (or mixed). Setting the format to '0' // here puts the LDateTime object into "guess" mode based on the // data. Which is going to be the closest we get to recovering // from the ugly situation of not saving the date in a specific // format. dt->SetFormat(0); dt->Set(s); LAssert(dt->IsValid()); } Unlock(); return s != NULL; } /////////////////////////////////////////////////////////////////////////////// LMimeStream::LMimeStream() : LTempStream(ScribeTempPath(), 4 << 20) { } #ifdef _DEBUG #define DEBUG_MIME_STREAM 1 #else #define DEBUG_MIME_STREAM 0 #endif bool LMimeStream::Parse() { if (Tmp) Tmp->SetPos(0); #if DEBUG_MIME_STREAM LMemStream *Src = dynamic_cast(s); #endif bool Status = Text.Decode.Pull(s) != 0; if (!Status) { #if DEBUG_MIME_STREAM LFile f; if (Src && f.Open("c:\\temp\\mime_error.txt", O_WRITE)) { Src->SetSize(0); Src->SetPos(0); Src->Write(&f, (int)Src->GetSize()); f.Close(); } s->SetPos(0); #endif LAssert(!"Mime Parsing Failed."); } LTempStream::Empty(); return Status; } diff --git a/Code/ScribeUtils.cpp b/Code/ScribeUtils.cpp --- a/Code/ScribeUtils.cpp +++ b/Code/ScribeUtils.cpp @@ -1,2133 +1,2133 @@ #include "Scribe.h" #include "lgi/common/Http.h" #include "lgi/common/DocView.h" #include "lgi/common/Store3.h" #include "lgi/common/Button.h" #include "lgi/common/TableLayout.h" #include "lgi/common/TabView.h" #include "lgi/common/OpenSSLSocket.h" #include "lgi/common/LgiRes.h" #include "ScribeUtils.h" #include "ScribeDefs.h" #include "ScribeListAddr.h" #include "resdefs.h" #include "../src/common/Coding/ScriptingPriv.h" #define COMP_FUNCTIONS 1 #include "lgi/common/ZlibWrapper.h" static char Ws[] = " \t\r\n"; #include "chardet.h" LString DetectCharset(LString s) { DetectObj *obj = detect_obj_init (); if (!obj) return NULL; LString cs; if (detect_r(s.Get(), s.Length(), &obj) == CHARDET_SUCCESS && obj->confidence >= 0.75) cs = obj->encoding; LgiTrace("%s:%i - encoding=%s, obj->confidence=%f, obj->bom=%i, str='%s'\n", _FL, obj->encoding, obj->confidence, obj->bom, s.Get()); detect_obj_free (&obj); return cs; } const char *ScribeResourcePath() { static char Res[MAX_PATH_LEN] = {0}; if (!Res[0]) { #if defined(LINUX) // Check for AppImage location LFile::Path app(LSP_APP_INSTALL); app += "../../usr/share/applications"; if (app.Exists()) { strcpy_s(Res, sizeof(Res), app.GetFull()); LgiTrace("%s:%i - Res: %s.\n", _FL, Res); return Res; } // else LgiTrace("%s:%i - Warning: app image resource '%s' doesn't exist.\n", _FL, app.GetFull().Get()); // else fall through to portable mode #elif defined(MAC) // Find resource folder in app bundle LMakePath(Res, sizeof(Res), LGetExeFile(), "Contents/Resources"); return Res; #endif #if !defined(MAC) const char *Paths[] = { "./Resources", "../Resources", "../../Resources", }; bool Found = false; for (unsigned i=0; iGet(d, sizeof(d)); p.Push(d); break; } } } void PushArrayContent(LStringPipe &p, char *&s, LDom *Source) { // Process inside of array index while (s && *s) { // Skin ws while (*s && strchr(Ws, *s)) s++; // Is end of array brackets? if (*s == ']') { break; } else if (*s == '\'' || *s == '\"') { // String const char Delim = *s++; char *e = strchr(s, Delim); if (e) { p.Push(s, e-s); s = e + 1; } else { s += strlen(s); break; } } else { // Variable LStringPipe Var; char *e = s; int Depth = 0; while (*e && !strchr(Ws, *e)) { if (*e == '[') { e++; Var.Push(s, e-s); PushArrayContent(Var, e, Source); s = e; Depth++; } else if (*e == ']') { if (Depth > 0 || e[1] == '.') { // Continue the variable if (Depth) Depth--; e++; } else { // End the var break; } } else { e++; } } Var.Push(s, e-s); char *Tok = Var.NewStr(); if (Tok) { LVariant v; if (Source->GetValue(Tok, v)) { PushVariant(p, v); } else { p.Push(Tok); } DeleteArray(Tok); } s = e; } } } char *ScribeInsertFields(const char *Template, LDom *Source) { if (Template && Source) { LStringPipe p; char *n; for (const char *s=Template; s && *s; s = n) { n = strstr((char*)s, "') || strchr(Ws, *e) ) { break; } e++; } if (*e == '[') { e++; Var.Push(s, e-s); // Process inside of array index PushArrayContent(Var, e, Source); } else if (strchr(Ws, *e)) { // Skip whitespace Var.Push(s, e-s); while (*e && strchr(Ws, *e)) { e++; } } else if (e[0] == '?' && e[1] == '>') { // End of var Var.Push(s, e-s); GotEnd = true; n = e; break; } else { // error n = e;; break; } s = e; } if (GotEnd) { char *Name = Var.NewStr(); if (Name) { char *i = Name, *o = Name; while (*i) { if (*i == '=') { i++; char h[] = {i[0], i[1], 0}; *o++ = htoi(h); i += 2; } else { *o++ = *i++; } } *o++ = 0; LVariant v; if (Source->GetValue(Name, v)) { switch (v.Type) { default: break; case GV_STRING: { p.Push(v.Str()); break; } case GV_INT32: { char i[32]; sprintf_s(i, sizeof(i), "%i", v.Value.Int); p.Push(i); break; } case GV_DOUBLE: { char d[32]; sprintf_s(d, sizeof(d), "%f", v.Value.Dbl); p.Push(d); break; } case GV_DATETIME: { char d[64]; v.Value.Date->Get(d, sizeof(d)); p.Push(d); break; } } } DeleteArray(Name); } n += 2; } } else { p.Push(s); break; } } return p.NewStr(); } return 0; } ////////////////////////////////////////////////////////////////////////////////////// HttpImageThread::HttpImageThread(ScribeWnd *app, const char *proxy, LThreadTarget *First) : LThreadWorker(First, "HtmlImageLoader") { App = app; Proxy = proxy; Cache = ScribeTempPath(); if (!LDirExists(Cache)) FileDev->CreateFolder(Cache); } HttpImageThread::~HttpImageThread() { } void HttpImageThread::DoJob(LThreadJob *j) { LDocumentEnv::LoadJob *Job = dynamic_cast(j); if (!Job) return; char *d = strrchr(Job->Uri, '/'); if (!d) { Job->Status = LDocumentEnv::LoadJob::JobErr_Uri; Job->Error.Printf("No '/' in uri '%s'", Job->Uri.Get()); return; } LString CachedFile = UriMap.Find(Job->Uri); if (!CachedFile) { char *Ext = LGetExtension(++d); auto Qm = Ext ? strchr(Ext, '?') : NULL; auto Len = Qm ? Qm - Ext : Strlen(Ext); char p[MAX_PATH_LEN]; for (int i=0; i<1000; i++) { char Hash[256]; sprintf_s(Hash, sizeof(Hash), "%x_%i.%.*s", LHash((uchar*)d + 1, -1, true), i++, (int)Len, Ext); if (!LMakePath(p, sizeof(p), Cache, Hash)) { Job->Status = LDocumentEnv::LoadJob::JobErr_Path; Job->Error.Printf("MakePath failed: '%s' + '%s'", Cache.Get(), Hash); return; } if (!LFileExists(p)) break; } UriMap.Add(Job->Uri, CachedFile = p); } if (!LFileExists(CachedFile)) { const char *InHeaders = "User-Agent: Memecode Scribe\r\n" "Accept: text/html,application/xhtml+xml,application/xml,image/png,image/*;q=0.9,*/*;q=0.8\r\n" "Accept-Encoding: gzip, deflate\r\n"; LFile f; if (f.Open(CachedFile, O_READWRITE)) { LUri Prox(Proxy); bool r = LgiGetUri(this, &f, &Job->Error, Job->Uri, InHeaders, Proxy ? &Prox : NULL); f.Close(); if (!r) { Job->Status = LDocumentEnv::LoadJob::JobErr_GetUri; FileDev->Delete(CachedFile, false); } } else { Job->Status = LDocumentEnv::LoadJob::JobErr_FileOpen; } } if (LFileExists(CachedFile)) { LString::Array Mime = LGetFileMimeType(CachedFile).Split("/"); if (Mime[0].Equals("image")) { int Promote = GdcD->SetOption(GDC_PROMOTE_ON_LOAD, 0); Job->pDC.Reset(GdcD->Load(CachedFile)); GdcD->SetOption(GDC_PROMOTE_ON_LOAD, Promote); if (Job->pDC) { Job->Status = LDocumentEnv::LoadJob::JobOk; } else { char *d = strrchr(CachedFile, DIR_CHAR); Job->Error.Printf("%s:%i - LoadDC(%s) failed [%s].", _FL, d?d+1:CachedFile.Get(), Job->Uri.Get()); FileDev->Delete(CachedFile, false); Job->Status = LDocumentEnv::LoadJob::JobErr_ImageFilter; } } else { // Css??? LFile *f = new LFile; if (f) { if (f->Open(CachedFile, O_READ)) { Job->Stream.Reset(f); Job->Status = LDocumentEnv::LoadJob::JobOk; } else { Job->Status = LDocumentEnv::LoadJob::JobErr_FileOpen; Job->Error.Printf("%s:%i - Cant read from '%s' (err=%i).", _FL, CachedFile.Get(), f->GetError()); delete f; } } else Job->Status = LDocumentEnv::LoadJob::JobErr_NoMem; } } else if (!Job->Error) { Job->Status = LDocumentEnv::LoadJob::JobErr_NoCachedFile; Job->Error = "No file in cache"; } if (Job->Error) { LgiTrace("Image load failed: %s\n", Job->Error.Get()); } } char *ScribeTempPath() { static char Tmp[MAX_PATH_LEN] = ""; if (Tmp[0] == 0) { if (LGetSystemPath(LSP_TEMP, Tmp, sizeof(Tmp))) { LMakePath(Tmp, sizeof(Tmp), Tmp, "Scribe"); } else { LgiTrace("%s:%i - LgiGetSystemPath(LSP_TEMP) failed.\n", _FL); return NULL; } } if (!LDirExists(Tmp)) { LError Err; if (!FileDev->CreateFolder(Tmp, true, &Err)) { LgiTrace("%s:%i - CreateFolder(%s) failed with %i\n", _FL, Tmp, Err.GetCode()); return NULL; } } return Tmp; } void ClearTempPath() { char *Tmp = ScribeTempPath(); if (Tmp) { if (!LDirExists(Tmp)) FileDev->CreateFolder(Tmp); LDirectory d; for (int b = d.First(Tmp); b; b = d.Next()) { if (!d.IsDir()) { char p[256]; d.Path(p, sizeof(p)); FileDev->Delete(p, false); } } } } //////////////////////////////////////////////////////////////////////////////////////////////// #define BufferLen_64ToBin(l) ( ((l)*3)/4 ) #define BufferLen_BinTo64(l) ( ((((l)+2)/3)*4) ) int DecodeUuencodedChar(const char *&s) { int Status = -1; if (*s == 0x60) { Status = 0; s++; } else if (*s >= (' ' + 64) || *s < ' ') { printf("%s:%i - Invalid uuencode char: %c (%i)\n", _FL, *s, (uchar)*s); } else { Status = *s - ' '; s++; } return Status; } bool DecodeUuencodedLine(LStreamI *Out, const char *Text, ssize_t Len) { bool Status = false; if (Text && Len > 1) { uchar *Buf = new uchar[Len]; if (Buf) { const char *End = Text + Len; const char *c = Text; uchar *d = Buf; int Count = DecodeUuencodedChar(c); int Processed = 0; if (Count < 0) return false; while (c < End && *c) { int t[4]; // De-text t[0] = DecodeUuencodedChar(c); if (t[0] < 0) break; t[1] = DecodeUuencodedChar(c); if (t[1] < 0) break; t[2] = DecodeUuencodedChar(c); if (t[2] < 0) break; t[3] = DecodeUuencodedChar(c); if (t[3] < 0) break; // Convert to binary uchar b[3] = { (uchar) ((t[0] << 2) | ((t[1] & 0x30) >> 4)), (uchar) (((t[1] & 0xF) << 4) | ((t[2] & 0x3C) >> 2)), (uchar) (((t[2] & 0x3) << 6) | (t[3])) }; // Push onto the output stream switch (Count - Processed) { case 1: { *d++ = b[0]; Processed++; break; } case 2: { *d++ = b[0]; *d++ = b[1]; Processed += 2; break; } default: { if (Count - Processed >= 3) { *d++ = b[0]; *d++ = b[1]; *d++ = b[2]; Processed += 3; } break; } } } if (Processed != Count) { printf("%s:%i - uuencode line error, processed %i of %i\n", _FL, Processed, Count); } Status = Out->Write(Buf, d-Buf) > 0; DeleteArray(Buf); } } return Status; } bool DecodeUuencodedAttachment(LDataStoreI *Store, LArray &Files, LStreamI *Out, const char *In) { if (Store && In) { // const char Ws[] = " \t\r\n"; LStringPipe FileName; LAutoPtr FileData; const char *e; const char *Last = In; int Line = 1; for (const char *s = In; s && *s; s = *e?e+1:e) { // Find the end of the line... e = s; while (*e && *e != '\n') e++; if (FileData) { if (_strnicmp(s, "end", 3) == 0) { // Write attachment LDataI *Attachment = Store->Create(MAGIC_ATTACHMENT); if (Attachment) { LAutoString Name(FileName.NewStr()); if (Name) { char *e = Name + strlen(Name); while (e > Name.Get() && strchr(" \t\r\n", e[-1])) *--e = 0; Attachment->SetStr(FIELD_NAME, Name); } LAutoStreamI fd(FileData.Release()); Attachment->SetStream(fd); Files.Add(Attachment); } FileData.Reset(); } else if (!DecodeUuencodedLine(FileData, s, e - s)) { /* printf("%s:%i - DecodeUuencodedLine failed on line %i:\n\t%s\n", _FL, Line, s); */ } } // Is it the start of a file else if (_strnicmp(s, "begin ", 6) == 0) { if (Last) { Out->Write(Last, s - Last); Last = 0; } - LToken Header(s, " ", true, e - s); + auto Header = LString(s, e - s).SplitDelimit(" ", -1, true); if (Header.Length() >= 3) { LMemQueue File; for (int n=2; Header[n]; n++) { FileName.Print("%s%s", n==2?"":" ", Header[n]); } } FileData.Reset(new LStringPipe(256)); } else if (!Last) { Last = s; } Line++; } if (Files.Length() && Last) { Out->Write(Last, strlen(Last)); } } return Files.Length() > 0; } char *MakeFileName(const char *ContentUtf, const char *Ext) { if (!ContentUtf) { LAssert(!"Invalid parameter."); return 0; } char *Content = 0; if (LIsUtf8(ContentUtf)) { // Valid UTF-8 Content = NewStr(ContentUtf); } else { // Garbage, so just ignore the input data. char n[256]; sprintf_s(n, sizeof(n), "_%i", LRand(1000000)); Content = NewStr(n); } if (!Content) { LAssert(!"No content to make filename from."); return 0; } char File[MAX_PATH_LEN]; char *e = Content; for (int i=0; i<64 && *e; i++) { char *before = e; e = LSeekUtf8(e, 1); if (e == before) { LAssert(!"LSeekUtf8 failed to more pointer forward."); break; } } *e = 0; if (strlen(Content) > 0) { if (Ext) sprintf_s(File, sizeof(File), "%s.%s", Content, Ext); else sprintf_s(File, sizeof(File), "%s", Content); } else { LAssert(!"No content for file name?"); strcpy_s(File, sizeof(File), "file"); } // Strip out invalid characters... char *Out = File; for (char *In = File; *In; In++) { if (!strchr("\\/?*:\"<>|\r\n", *In)) { *Out++ = *In; } } *Out++ = 0; LAssert(strlen(File) > 0); char Temp[MAX_PATH_LEN]; LMakePath(Temp, sizeof(Temp), ScribeTempPath(), File); if (LFileExists(Temp)) { char *Dot = strrchr(Temp, '.'); for (int i=2; LFileExists(Temp); i++) { ssize_t Len = Dot - Temp; sprintf_s(Dot, sizeof(Temp)-Len, "%i.%s", i, Ext); } } DeleteArray(Content); return NewStr(Temp); } /////////////////////////////////////////////////////////////////////////////////////////// Store3Progress::Store3Progress(LView *parent, bool interact) : LProgressDlg(parent) { Interact = interact; NewFormat = -1; } const char *Store3Progress::GetStr(int id) { switch (id) { case Store3UiError: return Err; case Store3UiStatus: return (Cache = ItemAt(0)->GetDescription()); } LAssert(0); return 0; } Store3Status Store3Progress::SetStr(int id, const char *str) { switch (id) { case Store3UiError: Err = str; // Fall through case Store3UiStatus: ItemAt(0)->SetDescription(str); return Store3Success; } LAssert(0); return Store3Error; } int64 Store3Progress::GetInt(int id) { switch (id) { case Store3UiCurrentPos: return ItemAt(0)->Value(); case Store3UiInteractive: return Interact; case Store3UiCancel: return IsCancelled(); case Store3UiNewFormat: return NewFormat; } LAssert(0); return -1; } Store3Status Store3Progress::SetInt(int id, int64 i) { switch (id) { case Store3UiCancel: return Store3Error; case Store3UiCurrentPos: ItemAt(0)->Value(i); break; case Store3UiMaxPos: ItemAt(0)->SetRange(i); break; case Store3UiNewFormat: NewFormat = (int)i; break; default: LAssert(0); return Store3Error; } return Store3Success; } /////////////////////////////////////////////////////////////////////// class BufferedTrace { List Traces; public: ~BufferedTrace() { for (auto s: Traces) { LgiTrace(s); DeleteArray(s); } } void Trace(char *s) { if (s) { Traces.Insert(NewStr(s)); } } } ; static BufferedTrace Bt; void TraceTime(char *s) { static int64 Last = 0; if (s) { int64 Now = LCurrentTime(); int64 Diff = 0; if (Last) { Diff = Now - Last; } else { Diff = 0; } Last = Now; char m[256]; sprintf_s(m, sizeof(m), "%s (+%i)", s, (int)Diff); Bt.Trace(m); } else { Last = 0; } } ///////////////////////////////////////////////////////////////////////////// Counter::~Counter() { for (auto c: *this) { DeleteObj(c); } } CountItem *Counter::FindType(int Type) { for (auto c: *this) { if (Type == c->Type) { return c; } } CountItem *c = new CountItem; if (c) { c->Type = Type; Insert(c); } return c; } void Counter::Inc(int Type) { CountItem *c = FindType(Type); if (c) { c->Count++; } } void Counter::Dec(int Type) { CountItem *c = FindType(Type); if (c) { c->Count--; } } void Counter::Add(int Type, int64 n) { CountItem *c = FindType(Type); if (c) { c->Count += n; } } void Counter::Sub(int Type, int64 n) { CountItem *c = FindType(Type); if (c) { c->Count -= n; } } int64 Counter::GetTypeCount(int Type) { CountItem *c = FindType(Type); if (c) { return c->Count; } return 0; } ////////////////////////////////////////////////////////// ItemFieldDef *ScribeGetFieldDefs(int Type) { switch ((uint32_t)Type) { case MAGIC_MAIL: { return MailFieldDefs; } case MAGIC_CONTACT: { return ContactFieldDefs; } case MAGIC_CALENDAR: { return CalendarFields; } } return 0; } Contact *IsContact(LListItem *Item) { return dynamic_cast(Item); } Mail *IsMail(LListItem *Item) { return dynamic_cast(Item); } ////////////////////////////////////////////////////////////////////////////// char sMimeVCard[] = "text/x-vcard"; char sMimeVCalendar[] = "text/calendar"; char sMimeICalendar[] = "application/ics"; char sMimeMbox[] = "text/mbox"; char sMimeLgiResource[] = "application/x-lgi-resource"; char sMimeMessage[] = "message/rfc822"; char sMimeXml[] = "text/xml"; LString ScribeGetFileMimeType(const char *File) { LString Ret; if (File) { char *Ext = LGetExtension((char*)File); if (Ext) { if (_stricmp(Ext, "lr8") == 0) { Ret = sMimeLgiResource; } else if (_stricmp(Ext, "ici") == 0) { Ret = "application/x-ici"; } else if (_stricmp(Ext, "vcf") == 0) { Ret = sMimeVCard; } else if (_stricmp(Ext, "vcs") == 0 || _stricmp(Ext, "ics") == 0) { Ret = sMimeVCalendar; } else if (_stricmp(Ext, "eml") == 0) { Ret = sMimeMessage; } #if defined WIN32 // Hard code extensions (because windows doesn't get it right) else if (_stricmp(Ext, "mbx") == 0 || _stricmp(Ext, "mbox") == 0) { Ret = sMimeMbox; } #endif } if (!Ret) { // Do normal lookup Ret = LGetFileMimeType(File); } } return Ret; } ///////////////////////////////////////////////////////////////////// Mailto::Mailto(ScribeWnd *app, const char *s) { App = app; Subject = NULL; Body = NULL; if (!s) return; // Do some detection of what type of string this is... // // Could be in various formats: // 1. user@isp.com // 2. user@isp.com, user2@isp.com, user3@isp.com // 3. "First Last" // 4. "First Last" , "First2 Last2" // 5. mailto:user@isp.com // 6. mailto:user@isp.com?subject=xxxxxx&body=xxxxxxxx // Skip whitespace while (*s && strchr(" \t\r\n", *s)) s++; // Check for mailto prefix if (_strnicmp(s, "mailto:", 7) == 0) { // Parse mailto URI char *e = NewStr(s + 7); char *In, *Out = e; for (In = e; *In; ) { if (In[0] == '%' && In[1] && In[2]) { char h[3] = { In[1], In[2], 0 }; *Out++ = htoi(h); In += 3; } else { *Out++ = *In++; } } *Out++ = 0; // Process mailto syntax char *Question = strchr(e, '?'); if (Question) { *Question++ = 0; // Split all the headers up - LToken Headers(Question, "&"); + auto Headers = LString(Question).SplitDelimit("&"); for (unsigned h=0; hsAddr = e; To.Insert(la); } } DeleteArray(e); } else { // Not a mailto, apply normal email recipient parsing char White[] = " \t\r\n"; #define SkipWhite(s) while (*s && strchr(White, *s)) s++; const char *Addr = s; for (const char *c = s; true;) { SkipWhite(c); if (*c == '\'' || *c == '\"') { char Delim = *c++; char *e = strchr((char*)c, Delim); if (e) c = e + 1; else c += strlen(c); } else if (*c == '<') { char *e = strchr((char*)c, '>'); if (e) c = e + 1; else c++; } else if (*c == ',' || *c == 0) { char *a = NewStr(Addr, c - Addr); if (a) { LAutoString Name, Addr; DecodeAddrName(a, Name, Addr, 0); if (Name || Addr) { ListAddr *la = new ListAddr(App); if (la) { if (Name && Addr) { la->sName = Name.Get(); la->sAddr = Addr.Get(); } else { la->sAddr = Name ? Name.Get() : Addr.Get(); } To.Insert(la); } } DeleteArray(a); } if (!*c) break; else { c++; SkipWhite(c); Addr = c; } } else c++; } } } Mailto::~Mailto() { To.DeleteObjects(); DeleteArray(Subject); DeleteArray(Body); } void Mailto::Apply(Mail *m) { if (m) { bool Dirty = false; if (Subject) { m->SetSubject(Subject); Dirty = true; } if (Body) { LVariant HtmlEdit; m->App->GetOptions()->GetValue(OPT_EditControl, HtmlEdit); auto Email = m->GetFromStr(FIELD_EMAIL); ScribeAccount *Acc = m->App->GetAccountByEmail(Email); if (!Acc) Acc = m->App->GetCurrentAccount(); LVariant Sig; LString Content; if (Acc) { if (HtmlEdit.CastInt32()) { Sig = Acc->Identity.HtmlSig(); if (Sig.Str()) { char *s = Sig.Str(); char *e = stristr(s, ""); if (e) Content.Printf("%.*s\n%s\n%s", e - s, s, Body, e + 6); else Content.Printf("%s\n%s", Body, s); } else Content = Body; } else { Sig = Acc->Identity.TextSig(); Content.Printf("%s\n%s", Body, Sig.Str()); } } if (HtmlEdit.CastInt32()) m->SetHtml(Content); else m->SetBody(Content); Dirty = true; } for (auto t: To) { LDataIt To = m->GetObject()->GetList(FIELD_TO); if (To) { LDataPropI *Addr = To->Create(m->GetObject()->GetStore()); if (Addr) { LDataPropI *p = dynamic_cast(t); if (p) { Addr->CopyProps(*p); To->Insert(Addr); } else { LAssert(!"Not the right object."); DeleteObj(Addr); } } if (m->GetUI()) { m->GetUI()->AddRecipient(new ListAddr(App, t)); } Dirty = true; } } if (Dirty) m->SetDirty(); } } ScribeDom::ScribeDom(ScribeWnd *a) { App = a; Email = NULL; Con = NULL; Cal = NULL; Fil = NULL; Grp = NULL; } bool ScribeDom::GetVariant(const char *Name, LVariant &Value, const char *Array) { ScribeDomType Fld = StrToDom(Name); switch (Fld) { case SdScribe: // Type: ScribeWnd { Value = (LDom*)App; break; } case SdMail: // Type: Mail { Value = Email; break; } case SdContact: // Type: Contact { Value = Con; break; } case SdContactGroup: // Type: ContactGroup { Value = Grp; break; } case SdCalendar: // Type: Calendar { Value = Cal; break; } case SdFilter: // Type: Filter { Value = Fil; break; } case SdNow: // Type: String { char n[256]; LDateTime Now; Now.SetNow(); Now.Get(n, sizeof(n)); Value = n; break; } default: { return false; } } return true; } ////////////////////////////////////////////////////////////////////////////////////// #include "lgi/common/Html.h" #include "lgi/common/Button.h" class HtmlMsg : public LDialog, public LDefaultDocumentEnv { LTableLayout *Tbl = NULL; Html1::LHtml *Html2 = NULL; public: HtmlMsg(LViewI *Parent, const char *Html, const char *Title, int Type) { LPoint Size(300, 300); SetParent(Parent); Name(Title?Title:"Message"); AddView(Tbl = new LTableLayout(2222)); auto c = Tbl->GetCell(0, 0); if (c->Add(Html2 = new Html1::LHtml(100, 0, 0, (int)(GdcD->X() * 0.5), (int)(GdcD->Y() * 0.75), this))) { Html2->SetCharset("utf-8"); Html2->Name(Html); /* Size = Html2->Layout(); LRect r(0, 0, Size.x, Size.y); Html2->SetPos(r); */ } LArray Btns; switch (Type & 0xf) { case MB_OK: Btns.Add(new LButton(IDOK, 0, 0, -1, -1, "Ok")); break; case MB_OKCANCEL: Btns.Add(new LButton(IDOK, 0, 0, -1, -1, "Ok")); Btns.Add(new LButton(IDCANCEL, 0, 0, -1, -1, "Cancel")); break; case MB_YESNO: Btns.Add(new LButton(IDYES, 0, 0, -1, -1, "Yes")); Btns.Add(new LButton(IDNO, 0, 0, -1, -1, "No")); break; case MB_YESNOCANCEL: Btns.Add(new LButton(IDYES, 0, 0, -1, -1, "Yes")); Btns.Add(new LButton(IDNO, 0, 0, -1, -1, "No")); Btns.Add(new LButton(IDCANCEL, 0, 0, -1, -1, "Cancel")); break; } c = Tbl->GetCell(0, 1); c->TextAlign(LCss::AlignCenter); for (auto b: Btns) c->Add(b); LRect r(0, 0, Size.x + 20 + LAppInst->GetMetric(LGI_MET_DECOR_X), Size.y + 20 + LSysFont->GetHeight() + LAppInst->GetMetric(LGI_MET_DECOR_CAPTION) + LAppInst->GetMetric(LGI_MET_DECOR_Y)); SetPos(r); MoveSameScreen(Parent); } void OnPosChange() { if (Tbl) Tbl->SetPos(GetClient()); } int OnNotify(LViewI *Ctrl, LNotification n) { switch (Ctrl->GetId()) { case IDOK: case IDCANCEL: case IDYES: case IDNO: EndModal(Ctrl->GetId()); break; } return LDialog::OnNotify(Ctrl, n); } }; void LHtmlMsg(std::function Callback, LViewI *Parent, const char *Html, const char *Title, int Type, ...) { va_list Arg; va_start(Arg, Type); #undef vsnprintf int length = vsnprintf(NULL, 0, Html, Arg); LAutoString Msg(new char[++length]); vsprintf_s(Msg, length, Html, Arg); va_end(Arg); auto Dlg = new HtmlMsg(Parent, Msg, Title, Type); Dlg->DoModal([Callback](auto dlg, auto id) { if (Callback) Callback(id); delete dlg; }); } ///////////////////////////////////////////////////////////////////////////// void TabDialog::OnCreate() { LTabView *Tab; if (GetViewById(TabCtrlId, Tab)) { Tab->SetPourChildren(true); LRect r(0, 0, 100, 100); Tab->SetPos(r); } OnPosChange(); } void TabDialog::IdealSize(LButton *b) { LViewLayoutInfo Inf; if (b->OnLayout(Inf)) { b->OnLayout(Inf); } else if (b->GetWindow()) { auto s = b->GetWindow()->GetDpiScale(); LDisplayString ds(b->GetFont(), b->Name()); Inf.Width.Max = (int32)(ds.X() + (s.x * LButton::Overhead.x)); Inf.Height.Max = (int32)(ds.Y() + (s.y * LButton::Overhead.y)); } else { LAssert(!"No way to set ideal size."); return; } LRect p = b->GetPos(); p.SetSize(Inf.Width.Max, Inf.Height.Max); b->SetPos(p); } void TabDialog::OnPosChange() { LButton *Ok = 0, *Cancel = 0, *Help = 0; LViewI *Tab = 0; if (GetViewById(TabCtrlId, Tab) && GetViewById(IDOK, Ok) && GetViewById(IDCANCEL, Cancel)) { GetViewById(HelpBtnId, Help); LRect r = GetClient(); r.Inset(LTableLayout::CellSpacing, LTableLayout::CellSpacing); IdealSize(Ok); IdealSize(Cancel); LRect t = r; t.y2 -= LTableLayout::CellSpacing + Ok->Y(); Tab->SetPos(t); if (Help) { IdealSize(Help); LRect h = Help->GetPos(); h.Offset(r.x1 - h.x1, r.y2 - h.Y() + 1 - h.y1); Help->SetPos(h); } LRect c = Cancel->GetPos(); c.Offset(r.x2 - c.X() + 1 - c.x1, r.y2 - c.Y() + 1 - c.y1); Cancel->SetPos(c); LRect o = Ok->GetPos(); o.Offset(c.x1 - LTableLayout::CellSpacing - o.X() + 1 - o.x1, r.y2 - o.Y() + 1 - o.y1); Ok->SetPos(o); } } LAutoString ConvertThreadIndex(char *ThreadIndex, int TruncateChars) { LAutoString a; if (ThreadIndex) { uchar InBuf[256]; ssize_t In = ConvertBase64ToBinary(InBuf, sizeof(InBuf), ThreadIndex, strlen(ThreadIndex)); LAssert(In >= 22); LStringPipe OutBuf(256); for (int i=0; i, ScribeDomType> Scribe_StrToDom(0, SdNone); static LHashTbl, const char *> Scribe_DomToStr; void InitStrToDom() { if (Scribe_StrToDom.Length() == 0) { #undef _ #define _(name) Scribe_StrToDom.Add(#name, Sd##name); \ LAssert(Scribe_StrToDom.Find(#name) == Sd##name); \ Scribe_DomToStr.Add(Sd##name, #name); #include "DomTypeValues.h" #undef _ } } ScribeDomType StrToDom(const char *s) { ScribeDomType d = Scribe_StrToDom.Find(s); return d; } const char *DomToStr(ScribeDomType d) { const char *s = Scribe_DomToStr.Find(d); return s; } void PatternBox(LSurface *pDC, const LRect &r) { int All = r.X() + r.Y() - 1; int MinEdge = MIN(r.X(), r.Y()); bool Wider = r.X() > r.Y(); for (int i=0; i> 2) % 2; /* if (i <= 4) LgiTrace("pt=%i,%i draw=%i\n", pt.x, pt.y, Draw); */ if (!Draw) continue; if (i < MinEdge) { pDC->Line(r.x1, r.y1 + i, r.x1 + i, r.y1); } else if (Wider) { if (i < r.X()) { int yy = r.Y() - 1; pDC->Line(pt.x, pt.y, pt.x - yy, pt.y + yy); } else { int yy = r.Y() - (i - r.X() + 1) - 1; pDC->Line(r.x2, r.y2-yy, r.x2-yy, r.y2); } } else // Tall { if (i < r.Y()) { int xx = r.X() - 1; pDC->Line(r.x1, r.y1 + i, r.x1 + xx, r.y1 + i - xx); } else { int xx = r.X() - (i - r.Y() + 1) - 1; pDC->Line(r.x2-xx, r.y2, r.x2, r.y2-xx); } } } } /////////////////////////////////////////////////////////////////////////////////////// ContactGroup *LookupContactGroup(ScribeWnd *App, const char *Name) { auto Srcs = App->GetThingSources(MAGIC_GROUP); if (!Srcs.Length() || !Name) return NULL; for (auto s: Srcs) { s->LoadThings(); for (auto t: s->Items) { ContactGroup *g = t->IsGroup(); if (!g) continue; LVariant Nm; if (g->GetVariant("Name", Nm) && Nm.Str() && _stricmp(Nm.Str(), Name) == 0) { return g; } } } return NULL; } ////////////////////////////////////////////////////////////////////////////////////////////////// LOAuth2::Params GetOAuth2Params(const char *Host, Store3ItemTypes Context) { LOAuth2::Params p; // FYI: None of this works due to issues at the providers end. It did sometime in the // past. And is only here in case someone wants to try and get it working again. if (stristr(Host, "google.") || stristr(Host, "gmail.")) { if (Context == MAGIC_MAIL) { p.AuthUri = "https://accounts.google.com/o/oauth2/auth"; p.ApiUri = "https://www.googleapis.com/oauth2/v3/token"; #if 1 // Old scope: p.Scope = "https://mail.google.com/"; #else // New scope: (doesn't work) p.Scope = "https://www.googleapis.com/auth/gmail.modify"; #endif // p.RevokeUri = "https://accounts.google.com/o/oauth2/revoke"; } /* else if (Context == MAGIC_CALENDAR) { p.AuthUri = "https://accounts.google.com/o/oauth2/v2/auth"; p.ApiUri = "https://apidata.googleusercontent.com/caldav/v2/%s/user"; p.Scope = "https://www.googleapis.com/auth/calendar"; } */ else return p; p.Provider = LOAuth2::Params::OAuthGoogle; p.ClientID = ""; p.ClientSecret = ""; p.RedirURIs = "urn:ietf:wg:oauth:2.0:oob\nhttp://localhost"; } else if (stristr(Host, "outlook.") && !stristr(Host, "office365.")) { if (Context == MAGIC_MAIL) { p.RedirURIs = "urn:ietf:wg:oauth:2.0:oob\nhttp://localhost"; p.AuthUri = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize"; p.ApiUri = "https://login.microsoftonline.com/common/oauth2/v2.0/token"; } else return p; p.Provider = LOAuth2::Params::OAuthMicrosoft; p.ClientID = ""; p.ClientSecret = ""; p.Scope = "https://outlook.office.com/mail.readwrite%20https://outlook.office.com/mail.send"; } return p; } ////////////////////////////////////////////////////////////////////////////////////////////////// class ScribeHtmLParser : public LHtmlParser { LScriptEngine Eng; public: struct HtmlElem : public LHtmlElement { LHashTbl, LString> Attr; HtmlElem(LHtmlElement *e) : LHtmlElement(e) { } bool GetVariant(const char *Name, LVariant &Value, const char *Array = NULL) { if (!Stricmp(Name, "element")) { Value = Tag.Get(); return true; } else if (!Stricmp(Name, "content")) { Value.OwnStr(WideToUtf8(Txt.Get())); return true; } else if (!Stricmp(Name, "attr")) { if (Array) { char *s = Attr.Find(Array); if (s) { Value = s; return true; } } } return false; } bool Get(const char *attr, const char *&val) { auto s = Attr.Find(attr); if (!s) return false; val = s.Get(); return true; } void Set(const char *attr, const char *val) { Attr.Add(attr, val); } }; ScribeHtmLParser() : LHtmlParser(NULL), Eng(NULL, NULL, NULL) { } LHtmlElement *CreateElement(LHtmlElement *Parent) { return new HtmlElem(Parent); } void Evaluate(LArray &Out, LString Search, HtmlElem *Elem) { LVariant Result; if (Eng.EvaluateExpression(&Result, Elem, Search)) { if (Result.CastInt32()) Out.Add(Elem); } for (auto e: Elem->Children) Evaluate(Out, Search, dynamic_cast(e)); } }; bool SearchHtml(LVariant *ReturnValue, const char *Html, const char *SearchExp, const char *ResultExp) { ScribeHtmLParser Parser; ScribeHtmLParser::HtmlElem Root(NULL); if (!Parser.Parse(&Root, Html)) { LgiTrace("%s:%i - HTML parsing failed.\n", _FL); *ReturnValue = false; return false; } LArray Matches; Parser.Evaluate(Matches, SearchExp, &Root); if (!ReturnValue->SetList()) return false; LScriptEngine Eng(NULL, NULL, NULL); for (auto e: Matches) { LVariant *Result = new LVariant; Eng.EvaluateExpression(Result, e, ResultExp); ReturnValue->Add(Result); } return true; } ////////////////////////////////////////////////////////////////////////////////////////////////////////// ScriptDownloadContentThread::ScriptDownloadContentThread(ScribeWnd *app, LString uri, LString callbackName, LVariant *userData) : LThread("ScriptDownloadContentThread", (App = app)->AddDispatch()) { Uri = uri; CallbackName = callbackName; if (userData) UserData = *userData; DeleteOnExit = true; Run(); } int ScriptDownloadContentThread::Main() { Result = LgiGetUri(this, &Out, &Err, Uri); return false; } void ScriptDownloadContentThread::OnComplete() { auto Cb = App->GetCallback(CallbackName); if (!Cb.Func) return; LVirtualMachine Vm; LScriptArguments Args(&Vm); LVariant vApp((LDom*)App); Args.Add(&vApp); LVariant vUri = Uri.Get(); Args.Add(&vUri); LVariant vResult = Result; Args.Add(&vResult); LVariant vData; if (Result) vData.OwnStr(Out.NewStr()); else vData = Err.Get(); Args.Add(&vData); Args.Add(&UserData); App->ExecuteScriptCallback(Cb, Args); } //////////////////////////////////////////////////////////////////////////////////////////////////////// // This converts an async call to sync, because the GetVariant / CallMethod API // can't be changed to include a callback. It's a hack until such time as there // is proper support for callbacks in the DOM api. void WaitForVariant(LVariant &var) { auto StartTs = LCurrentTime(); while (var.Type == GV_NULL) { LSleep(10); LYield(); if (LCurrentTime() - StartTs > 20000) { LgiTrace("%s:%i - WaitForVariant waiting for: %is", _FL, (int)(LCurrentTime()-StartTs)); StartTs = LCurrentTime(); } } } void WaitForString(LString &var) { auto StartTs = LCurrentTime(); while (var.Get() == NULL) { LSleep(10); LYield(); if (LCurrentTime() - StartTs > 20000) { LgiTrace("%s:%i - WaitForString waiting for: %is", _FL, (int)(LCurrentTime()-StartTs)); StartTs = LCurrentTime(); } } }