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 = [&]() { + auto emitBuf = [&Blocks, &used, &buf]() { 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)) { 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,3290 @@ /*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) +void Calendar::SummaryOfToday(ScribeWnd *App, std::function Callback) { - bool Status = false; + LDateTime Now; + Now.SetNow(); + LDateTime Next = Now; + Next.AddMonths(1); LArray Sources; - if (App && App->GetCalendarSources(Sources)) - { - LDateTime Now; - Now.SetNow(); - LDateTime Next = Now; - Next.AddMonths(1); + if (!App || !Callback || !App->GetCalendarSources(Sources)) + return; - LArray e; - for (unsigned i=0; iGetEvents(Now, Next, e); + char s[256]; + sprintf_s(s, sizeof(s), "%s", LLoadString(IDS_NO_EVENTS)); + Callback(s); } - - if (e.Length()) + else { 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; - } + Callback(p.NewLStr()); } - 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)) { 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); + DstFolder->MoveTo(Items, false); + // FIXME: Impl moveto callback properly. 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/Calendar.h b/Code/Calendar.h --- a/Code/Calendar.h +++ b/Code/Calendar.h @@ -1,387 +1,433 @@ #ifndef __CALENDER_H #define __CALENDER_H // Forward class decl #include "lgi/common/vCard-vCal.h" #include "ScribeListAddr.h" #include "lgi/common/ListItemCheckBox.h" #include "lgi/common/ListItemRadioBtn.h" #include "lgi/common/TabView.h" class Calendar; class CalendarUi; class CalendarView; class Attendee; class CalendarSource; class LTimeLine; extern void InitCalendarView(); extern const char *RelativeTime(LDateTime &Then); enum CalRecurEndType { CalEndError, CalEndNever, CalEndOnCount, CalEndOnDate, }; enum CalRecurFreq { CalFreqDays, CalFreqWeeks, CalFreqMonths, CalFreqYears, }; // Classes struct TimePeriod { Calendar *c; CalendarSource *src; LDateTime s, e; TimePeriod() { c = NULL; src = NULL; } void ToLocal() { s.ToLocal(); e.ToLocal(); } bool Overlap(TimePeriod &p) { if (s >= p.e || e <= p.s) return false; return true; } bool Overlap(LDateTime &start, LDateTime &end) { if (s >= end || e <= start) return false; return true; } }; #include "Attendee.h" class ScribeClass Calendar : public Thing { friend class CalendarUi; friend class CalendarView; friend class CalendarSource; friend class CalendarTodoItem; // Static static List Reminders; // Data CalendarUi *Ui; LRegion ViewPos; CalendarSource *Source; class CalendarTodoItem *TodoView; LDateTime RecurAfter; LDateTime RemindTs; // Member bool GetParentSelection(LList *Lst, List &s); void OnPaintView(LSurface *pDC, LFont *Font, LRect *Pos, TimePeriod *Period); public: static void CheckReminders(); - static bool SummaryOfToday(ScribeWnd *App, LVariant &v); + static void SummaryOfToday(ScribeWnd *App, std::function Callback); // Week view static int DayStart; static int DayEnd; static int WorkDayStart; static int WorkDayEnd; static int WorkWeekStart; static int WorkWeekEnd; // Object Calendar(ScribeWnd *app, LDataI *object = 0); ~Calendar(); // Properties Store3ItemTypes Type() override { return MAGIC_CALENDAR; } void OnSerialize(bool Write) override; LColour GetColour(); CalendarView *GetView(); CalendarType GetCalType(); void SetCalType(CalendarType Type); CalendarSource *GetSource() { return Source; } bool GetTimes(LDateTime StartLocal, LDateTime EndLocal, LArray &Times); LDateTime *GetRemindTs() { return &RemindTs; } // Thing uint32_t GetFlags() override; ThingUi *DoUI(MailContainer *c = 0) override; ThingUi *GetUI() override; bool SetUI(ThingUi *ui = 0) override; void DoContextMenu(LMouse &m, LView *Parent = 0) override; void OnCreate() override; bool OnDelete() override; int Compare(LListItem *Arg, ssize_t FieldId) override; Calendar *IsCalendar() override { return this; } Thing &operator =(Thing &c) override; bool operator ==(Thing &c); // ListItem void OnMouseClick(LMouse &m) override; const char *GetText(int i) override; int *GetDefaultFields() override; const char *GetFieldText(int Field) override; // Misc bool Save(ScribeFolder *Folder = 0) override; bool Overlap(Calendar *c); // 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; // Property storage bool GetObjects(List &l); LDataI *NewObject(int Type); // Printing void OnPrintHeaders(struct ScribePrintContext &Context) override; void OnPrintText(ScribePrintContext &Context, LPrintPageRanges &Pages) 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; }; class CalendarUi : public ThingUi, public LDefaultDocumentEnv, public LResourceLoad { friend class Calendar; struct CalendarUiPriv *d; Calendar *Item; LTabView *Tabs; LTabPage *Appointment; LTabPage *AttendTab; LScriptUi Commands; class LAttendees *Attendees; bool NotifyOn; bool OnViewKey(LView *v, LKey &k); void CheckConsistancy(); LDateTime CurrentStart(); LDateTime CurrentEnd(); void UpdateStartRelative(); void UpdateEndRelative(); void UpdateRelative(); public: CalendarUi(Calendar *item); ~CalendarUi(); const char *GetClass() { return "CalendarUi"; } Calendar *GetCal() { return Item; } int OnCommand(int Cmd, int Event, OsView Window); int OnNotify(LViewI *Ctrl, LNotification n); void OnPosChange(); void OnLoad(); void OnSave(); }; class CalendarSource : public LListItem { protected: ScribeWnd *App; LString Id; int Display; LColour Colour; void SaveAttr(LXmlTag *t, const char *attr, const char *val) { if (ValidStr(val)) t->SetAttr(attr, val); else t->DelAttr(attr); } void SetParentFolder(Calendar *c, ScribeFolder *f) { if (c) c->SetParentFolder(f); } void SetCalendarsSource(Calendar *c) { if (c) c->Source = this; } static CalendarSource *CreateIn; static LArray AllSources; LString GetKey(); public: static CalendarSource *GetCreateIn() { return CreateIn; } static void SetCreateIn(CalendarSource *New); static const LArray &GetSources() { return AllSources; } static LColour FindUnusedColour(); static void FolderDelete(ScribeFolder *f); static constexpr const char *OptPath = "Path"; static constexpr const char *OptUri = "Uri"; static constexpr const char *OptDisplay = "Display"; static constexpr const char *OptColour = "Colour"; static constexpr const char *OptObject = "Object"; static CalendarSource *Create(ScribeWnd *app, const char *ObjName, const char *Id); CalendarSource() { App = NULL; Display = true; AllSources.Add(this); } virtual ~CalendarSource() { AllSources.Delete(this); if (CreateIn == this) SetCreateIn(NULL); } auto *GetApp() { return App; } virtual void EditPath(LView *parent, CalendarView *cv) = 0; virtual bool Delete() = 0; virtual bool Write() = 0; virtual bool Match(char *Email) = 0; virtual LColour GetColour(); virtual void SetColour(LColour c) = 0; virtual const char *GetName() = 0; virtual bool Read() = 0; - virtual bool GetEvents(LDateTime &Start, LDateTime &End, LArray &Events) = 0; + virtual bool GetEvents(const LDateTime Start, const LDateTime End, std::function&)> Callback) = 0; virtual Calendar *NewEvent() = 0; virtual void OnFolderDelete(ScribeFolder *f) = 0; virtual void OnPulse() = 0; }; +/// Helper class to collect events from multiple CalendarSource objects +class CalendarSourceGetEvents +{ + ScribeWnd *App = NULL; + LArray Sources; + LArray Events; + LDateTime Start, End; + std::function&)> Callback; + + void OnFinished() + { + if (Callback) + Callback(Events); + delete this; + } + +public: + CalendarSourceGetEvents(LDateTime start, + LDateTime end, + LArray sources, + std::function&)> callback) + { + Sources = sources; + Start = start; + End = end; + Callback = callback; + + for (auto src: Sources) + { + if (!App) + App = src->GetApp(); + + src->GetEvents(Start, End, [this, src](auto events) + { + LAssert(Sources.HasItem(src)); + Sources.Delete(src); + + Events += events; + + if (Sources.Length() == 0) + OnFinished(); + }); + } + } +}; + class FolderCalendarSource : public CalendarSource { protected: LString Path; ScribeFolder *Folder; public: FolderCalendarSource(ScribeWnd *a, const char *id = NULL); ~FolderCalendarSource(); const char *GetClass() { return "FolderCalendarSource"; } // Actions bool Read(); bool Write(); bool Delete(); Calendar *NewEvent(); bool Match(char *Email); - bool GetEvents(LDateTime &Start, LDateTime &End, LArray &Events); + bool GetEvents(const LDateTime Start, const LDateTime End, std::function&)> Callback); void EditPath(LView *parent, CalendarView *cv); // Props const char *GetPath() { return Path; } void SetPath(const char *Path); void SetColour(LColour c); int GetDisplay() { return Display; } void SetDisplay(int d) { Display = d; Update(); } const char *GetName() { return Path; } // Events void OnMouseClick(LMouse &m); void OnPaintColumn(LItem::ItemPaintCtx &Ctx, int i, LItemColumn *c); const char *GetText(int i); void OnChange(bool IsDelete); void OnFolderDelete(ScribeFolder *f); void OnPulse(); }; class RemoteCalendarSource : public CalendarSource { struct RemoteCalendarSourcePriv *d; public: RemoteCalendarSource(ScribeWnd *a, const char *id = NULL); ~RemoteCalendarSource(); const char *GetClass() { return "RemoteCalendarSource"; } // Actions bool Read(); bool Write(); bool Delete(); Calendar *NewEvent(); bool Match(char *Email); - bool GetEvents(LDateTime &Start, LDateTime &End, LArray &Events); + bool GetEvents(const LDateTime Start, const LDateTime End, std::function&)> Callback); void EditPath(LView *parent, CalendarView *cv); // Props const char *GetUri(); void SetUri(const char *uri); LColour GetColour(); void SetColour(LColour c); const char *GetName(); // Events void OnMouseClick(LMouse &m); void OnPaintColumn(LItem::ItemPaintCtx &Ctx, int i, LItemColumn *c); const char *GetText(int i); void OnFolderDelete(ScribeFolder *f); void OnPulse(); void OnChange(bool IsDelete); LMessage::Result OnEvent(LMessage *Msg); }; class CalendarTodoItem : public LListItem { friend int TodoCompare(LListItem *la, LListItem *lb, NativeInt d); ScribeWnd *App; Calendar *Todo; bool EditingLabel; char DateCache[40]; LListItemCheckBox *Done; void SetTodo(Calendar *todo); void OnColumnNotify(int Col, int64 Data); public: CalendarTodoItem(ScribeWnd *app, Calendar *todo = 0); ~CalendarTodoItem(); const char *GetText(int Col); bool SetText(const char *s, int Col); void OnPaint(ItemPaintCtx &Ctx); void OnMouseClick(LMouse &m); Calendar *GetTodo() { return Todo; } void Resort(); }; #endif diff --git a/Code/CalendarView.cpp b/Code/CalendarView.cpp --- a/Code/CalendarView.cpp +++ b/Code/CalendarView.cpp @@ -1,3462 +1,3485 @@ #include "Scribe.h" #include "lgi/common/ListItemCheckBox.h" #include "lgi/common/ListItemRadioBtn.h" #include "lgi/common/ColourSelect.h" #include "lgi/common/DropFiles.h" #include "lgi/common/MonthView.h" #include "lgi/common/YearView.h" #include "lgi/common/Array.h" #include "lgi/common/ScrollBar.h" #include "lgi/common/MonthView.h" #include "lgi/common/SkinEngine.h" #include "lgi/common/Combo.h" #include "lgi/common/DisplayString.h" #include "lgi/common/Printer.h" #include "lgi/common/Notifications.h" #include "lgi/common/Box.h" #include "lgi/common/LgiRes.h" #include "CalendarView.h" #include "ScribePageSetup.h" #include "Store3Webdav/WebdavStore.h" #include "../Resources/resdefs.h" #include "resource.h" char ScribeCalendarObject[] = "com.memecode.Calendar"; #define BORDER_YEAR 28 #define THRESHOLD_EDGE 3 // Px #define WEEKEND_TINT_LEVEL 0.97f #define WEEKEND_TINT_COLOUR LColour(0, 0, 0xd0) #define TODAY_TINT_LEVEL 0.95f #define TODAY_TINT_COLOUR LColour(255, 0, 0) ///////////////////////////////////////////////////////////////////////////////////// #define IDM_CAL_DAY 100 #define IDM_CAL_WEEK 101 #define IDM_CAL_MONTH 102 #define IDM_CAL_YEAR 103 #define IDM_PREV 200 #define IDM_BACK 201 #define IDM_TODAY 202 #undef IDM_FORWARD #define IDM_FORWARD 203 #define IDM_NEXT 204 #define IDM_CONFIG 205 #define IDM_TODO 206 #define IDM_15MIN 207 #define IDM_30MIN 208 #define IDM_1HR 209 #define IDM_NEW_EVENT 210 #define IDC_TODO 300 ///////////////////////////////////////////////////////////////////////////////////// void LoadCalendarStringTable() { ShortDayNames[0] = LLoadString(IDS_CAL_SDAY_SUN); ShortDayNames[1] = LLoadString(IDS_CAL_SDAY_MON); ShortDayNames[2] = LLoadString(IDS_CAL_SDAY_TUE); ShortDayNames[3] = LLoadString(IDS_CAL_SDAY_WED); ShortDayNames[4] = LLoadString(IDS_CAL_SDAY_THU); ShortDayNames[5] = LLoadString(IDS_CAL_SDAY_FRI); ShortDayNames[6] = LLoadString(IDS_CAL_SDAY_SAT); FullDayNames[0] = LLoadString(IDS_CAL_LDAY_SUN); FullDayNames[1] = LLoadString(IDS_CAL_LDAY_MON); FullDayNames[2] = LLoadString(IDS_CAL_LDAY_TUE); FullDayNames[3] = LLoadString(IDS_CAL_LDAY_WED); FullDayNames[4] = LLoadString(IDS_CAL_LDAY_THU); FullDayNames[5] = LLoadString(IDS_CAL_LDAY_FRI); FullDayNames[6] = LLoadString(IDS_CAL_LDAY_SAT); ShortMonthNames[0] = LLoadString(IDS_CAL_SMONTH_JAN); ShortMonthNames[1] = LLoadString(IDS_CAL_SMONTH_FEB); ShortMonthNames[2] = LLoadString(IDS_CAL_SMONTH_MAR); ShortMonthNames[3] = LLoadString(IDS_CAL_SMONTH_APR); ShortMonthNames[4] = LLoadString(IDS_CAL_SMONTH_MAY); ShortMonthNames[5] = LLoadString(IDS_CAL_SMONTH_JUN); ShortMonthNames[6] = LLoadString(IDS_CAL_SMONTH_JUL); ShortMonthNames[7] = LLoadString(IDS_CAL_SMONTH_AUG); ShortMonthNames[8] = LLoadString(IDS_CAL_SMONTH_SEP); ShortMonthNames[9] = LLoadString(IDS_CAL_SMONTH_OCT); ShortMonthNames[10] = LLoadString(IDS_CAL_SMONTH_NOV); ShortMonthNames[11] = LLoadString(IDS_CAL_SMONTH_DEC); FullMonthNames[0] = LLoadString(IDS_CAL_LMONTH_JAN); FullMonthNames[1] = LLoadString(IDS_CAL_LMONTH_FEB); FullMonthNames[2] = LLoadString(IDS_CAL_LMONTH_MAR); FullMonthNames[3] = LLoadString(IDS_CAL_LMONTH_APR); FullMonthNames[4] = LLoadString(IDS_CAL_LMONTH_MAY); FullMonthNames[5] = LLoadString(IDS_CAL_LMONTH_JUN); FullMonthNames[6] = LLoadString(IDS_CAL_LMONTH_JUL); FullMonthNames[7] = LLoadString(IDS_CAL_LMONTH_AUG); FullMonthNames[8] = LLoadString(IDS_CAL_LMONTH_SEP); FullMonthNames[9] = LLoadString(IDS_CAL_LMONTH_OCT); FullMonthNames[10] = LLoadString(IDS_CAL_LMONTH_NOV); FullMonthNames[11] = LLoadString(IDS_CAL_LMONTH_DEC); } ///////////////////////////////////////////////////////////////////////////////////// class LColourItem : public LListItemColumn { LListItem *Item; LView *Colour; public: LColourItem(LListItem *i, int c, COLOUR col = -1) : LListItemColumn(i, c) { Item = i; Colour = LViewFactory::Create("LColourSelect"); LAssert(Colour != NULL); LArray colours; for (int n=0; nSetColourList(&colours); Colour->Value(col); } } ~LColourItem() { DeleteObj(Colour); } void OnPaintColumn(ItemPaintCtx &Ctx, int i, LItemColumn *c) { if (!Colour->IsAttached()) { Colour->Attach(Item->GetList()); } Colour->SetPos(Ctx); Colour->Visible(true); } void OnMouseClick(LMouse &m) { Colour->OnMouseClick(m); } int64 Value() { return Colour->Value(); } void Value(int64 i) { Colour->Value(i); } void Disconnect() { Colour->Detach(); } }; ///////////////////////////////////////////////////////////////////////////////////// CalendarTodoItem::CalendarTodoItem(ScribeWnd *app, Calendar *todo) { App = app; Todo = 0; Done = 0; SetTodo(todo); EditingLabel = false; } CalendarTodoItem::~CalendarTodoItem() { if (Todo) { LAssert(Todo->TodoView == this); Todo->TodoView = 0; } } int TodoCompare(LListItem *la, LListItem *lb, NativeInt d) { int Status = 0; CalendarTodoItem *a = dynamic_cast(la); CalendarTodoItem *b = dynamic_cast(lb); if (a && b) { int Col = abs((int)d) - 1; int Mul = d >= 0 ? 1 : -1; bool Atodo = a->Todo != 0; bool Btodo = b->Todo != 0; if (Atodo ^ Btodo) { Status = Btodo - Atodo; } else if (a->Todo && b->Todo) { switch (Col) { case 0: { // Completed int Acomplete = 0; int Bcomplete = 0; a->Todo->GetField(FIELD_CAL_COMPLETED, Acomplete); b->Todo->GetField(FIELD_CAL_COMPLETED, Bcomplete); if (Acomplete != Bcomplete) { Status = Mul * (Acomplete - Bcomplete); } else { goto ByDueDate; } break; } case 1: { // Subject BySubject: const char *Asub = 0; const char *Bsub = 0; a->Todo->GetField(FIELD_CAL_SUBJECT, Asub); b->Todo->GetField(FIELD_CAL_SUBJECT, Bsub); if (Asub && Bsub) { Status = Mul * _stricmp(Asub, Bsub); } break; } case 2: { // Due date ByDueDate: LDateTime Adate; LDateTime Bdate; a->Todo->GetField(FIELD_CAL_START_UTC, Adate); b->Todo->GetField(FIELD_CAL_START_UTC, Bdate); bool Ad = Adate.Year() != 0; bool Bd = Bdate.Year() != 0; if (Ad ^ Bd) { Status = Mul * (Bd - Ad); } else if (Ad && Bd) { Status = Mul * Adate.Compare(&Bdate); } else { goto BySubject; } break; } } } } return Status; } void CalendarTodoItem::Resort() { if (GetList()) { int Sort = 1; for (int i=0; iGetColumns(); i++) { LItemColumn *c = GetList()->ColumnAt(i); if (c) { if (c->Mark() == GLI_MARK_UP_ARROW) { Sort = -(i + 1); break; } else if (c->Mark() == GLI_MARK_DOWN_ARROW) { Sort = i + 1; break; } } } GetList()->Sort(TodoCompare, Sort); } } void CalendarTodoItem::SetTodo(Calendar *todo) { if (Todo) { Todo->TodoView = 0; } Todo = todo; if (Todo) { Todo->TodoView = this; int Completed = false; Todo->GetField(FIELD_CAL_COMPLETED, Completed); Done = new LListItemCheckBox(this, 0, Completed != 0); } else { DeleteObj(Done); } SetImage(Todo ? ICON_TODO : -1); } const char *CalendarTodoItem::GetText(int Col) { if (Todo) { switch (Col) { case 1: { // Name const char *s; if (Todo->GetField(FIELD_CAL_SUBJECT, s)) { return s; } break; } case 2: { // Due LDateTime d; if (Todo->GetField(FIELD_CAL_START_UTC, d)) { d.ToLocal(true); d.Get(DateCache, sizeof(DateCache)); return DateCache; } break; } } } else if (Col == 1) { if (EditingLabel) { EditingLabel = false; } else { return (char*)LLoadString(IDS_CLICK_TO_CREATE); } } return 0; } bool CalendarTodoItem::SetText(const char *s, int Col) { if (Col == 1) { if (Todo) { // Set the name... Todo->SetField(FIELD_CAL_SUBJECT, (char*)s); Resort(); } else { if (ValidStr(s)) { // Create new todo ScribeFolder *Cal = App->GetFolder(FOLDER_CALENDAR); if (Cal) { Thing *t = App->CreateItem(MAGIC_CALENDAR, Cal, false); if (t && t->IsCalendar()) { SetTodo(t->IsCalendar()); Todo->SetCalType(CalTodo); Todo->SetField(FIELD_CAL_SUBJECT, (char*)s); GetList()->Insert(new CalendarTodoItem(App)); Resort(); } } else { LgiMsg(GetList(), "No calendar folder.", AppName); } } else { return false; } } } return LListItem::SetText(s, Col); } void CalendarTodoItem::OnPaint(ItemPaintCtx &Ctx) { if (!Todo) { Ctx.Fore = LColour(L_LOW); Ctx.Back = LColour(L_WORKSPACE); } else if (Done && Done->Value()) { Ctx.Fore = LColour(L_LOW); } else { LDateTime d, Now; if (Todo->GetField(FIELD_CAL_START_UTC, d)) { d.ToLocal(true); Now.SetNow(); if (Now.Compare(&d) > 0) { Ctx.Fore.Set(255, 0, 0); } } } LListItem::OnPaint(Ctx); } void CalendarTodoItem::OnColumnNotify(int Col, int64 Data) { if (Col == 0) { Todo->SetField(FIELD_CAL_COMPLETED, Data ? 100 : 0); Resort(); } } void CalendarTodoItem::OnMouseClick(LMouse &m) { LListItem::OnMouseClick(m); if (m.Down()) { if (m.Left()) { int Col = Todo ? GetList()->ColumnAtX(m.x) : 1; switch (Col) { case 1: { if (!Todo) { EditingLabel = true; } EditLabel(1); break; } default: { if (Todo && m.Double()) { Todo->DoUI(); } break; } } } else if (m.Right()) { if (Todo) { Todo->DoContextMenu(m, GetList()); } } } } ///////////////////////////////////////////////////////////////////////////////////// class LMonthView : public LView, public MonthView { LRect rTitle; LRect rCells; LRect rLeft, rRight; int Cell; CalendarView *CalView; public: LMonthView(int Id, LDateTime *n, CalendarView *calView); void OnCellClick(int Cx, int Cy); void SeekMonth(int Dir); void OnMouseClick(LMouse &m); void OnPaint(LSurface *pDC); }; // This structure encodes the direction to move the cursor // on different key presses. The 'years' isn't implemented in // the OnKey handler. struct CalViewArrow { public: CalendarViewMode Mode; int Key; int Flags; int Hours; int Days; int Months; int Years; }; CalViewArrow Arrows[] = { // Flags, H D M Y // Week view movement {CAL_VIEW_WEEK, LK_LEFT, 0, 0, -1, 0, 0}, {CAL_VIEW_WEEK, LK_RIGHT, 0, 0, 1, 0, 0}, {CAL_VIEW_WEEK, LK_UP, 0, -1, 0, 0, 0}, {CAL_VIEW_WEEK, LK_DOWN, 0, 1, 0, 0, 0}, {CAL_VIEW_WEEK, LK_PAGEUP, 0, 0, -7, 0, 0}, {CAL_VIEW_WEEK, LK_PAGEDOWN,0, 0, 7, 0, 0}, {CAL_VIEW_WEEK, LK_PAGEUP, LGI_EF_CTRL, 0, 0, -1, 0}, {CAL_VIEW_WEEK, LK_PAGEDOWN,LGI_EF_CTRL, 0, 0, 1, 0}, // Month view movement {CAL_VIEW_MONTH, LK_LEFT, 0, 0, -1, 0, 0}, {CAL_VIEW_MONTH, LK_RIGHT, 0, 0, 1, 0, 0}, {CAL_VIEW_MONTH, LK_UP, 0, 0, -7, 0, 0}, {CAL_VIEW_MONTH, LK_DOWN, 0, 0, 7, 0, 0}, {CAL_VIEW_MONTH, LK_PAGEUP, 0, 0, 0, -1, 0}, {CAL_VIEW_MONTH, LK_PAGEDOWN,0, 0, 0, 1, 0}, {CAL_VIEW_MONTH, LK_PAGEUP, LGI_EF_CTRL, 0, 0, 0, -1}, {CAL_VIEW_MONTH, LK_PAGEDOWN,LGI_EF_CTRL, 0, 0, 0, 1}, // Year view movement {CAL_VIEW_YEAR, LK_LEFT, 0, 0, -1, 0, 0}, {CAL_VIEW_YEAR, LK_RIGHT, 0, 0, 1, 0, 0}, {CAL_VIEW_YEAR, LK_UP, 0, 0, 0, -1, 0}, {CAL_VIEW_YEAR, LK_DOWN, 0, 0, 0, 1, 0}, {CAL_VIEW_YEAR, LK_PAGEUP, 0, 0, 0, 0, -1}, {CAL_VIEW_YEAR, LK_PAGEDOWN,0, 0, 0, 0, 1} }; CalViewArrow *GetModeArrow(CalendarViewMode m, LKey &k) { for (int i=0; i CalendarView::CalendarViews; CalendarView::CalendarView(ScribeFolder *folder, int id, LRect *pos, const char *name) { CalendarViews.Add(this); if (Calendar::DayStart < 0) InitCalendarView(); DragMode = DragNone; DragEvent = NULL; LastTsOffset = 0; DayStart = Calendar::DayStart; DayEnd = Calendar::DayEnd; if (!App) App = folder->App; Mode = CAL_VIEW_MONTH; LVariant v; if (App->GetOptions()->GetValue(OPT_CalendarViewMode, v)) Mode = (CalendarViewMode) v.CastInt32(); if (id > 0) SetId(id); if (pos) SetPos(*pos); if (name) Name(name); SetPourLargest(true); Sunken(true); MonthX = 7; MonthY = 5; LFontType Type; Type.GetSystemFont("Small"); Font.Reset(Type.Create()); Cursor.SetNow(); LoadUsers(); OnOptionsChange(); } CalendarView::~CalendarView() { LVariant v; App->GetOptions()->SetValue(OPT_CalendarViewMode, v = (int)Mode); CalendarViews.Delete(this); } void CalendarView::OnOptionsChange() { LVariant v; if (App && App->GetOptions()->GetValue(OPT_CalendarFirstDayOfWeek, v)) FirstDayOfWeek = v.CastInt32(); for (auto c: CalendarViews) { if (c->IsAttached()) c->Invalidate(); } } CalendarView *Calendar::GetView() { if (CalendarView::CalendarViews.Length() == 0) return NULL; return CalendarView::CalendarViews[0]; } void CalendarView::LoadUsers() { LArray Sources; App->GetCalendarSources(Sources); LVariant v; if (App->GetOptions()->GetValue(OPT_CalendarFirstDayOfWeek, v)) FirstDayOfWeek = v.CastInt32(); LDateTime dt = Cursor; Cursor.Set("1/1/1900"); SetCursor(dt); // Do we really need this? // OnContentsChanged(); } void CalendarView::DeleteSource(CalendarSource *cs) { // Delete events out of 'Current' for (unsigned i=0; iGetSource() == cs) { Current.DeleteAt(i--); } } // Delete the reference in the options file... cs->Delete(); // Delete the C++ object and list ref... DeleteObj(cs); // Refresh the screen. Invalidate(); } bool CalendarView::GetEventsBetween(LArray &Events, LDateTime Start, LDateTime End) { bool Status = false; LDateTime EndMinute = End; EndMinute.AddMinutes(-1); for (unsigned i=0; i(GetWindow()); LArray all; if (w->CalLst->GetAll(all)) { for (auto a: all) a->OnPulse(); } } bool CalendarView::OnLayout(LViewLayoutInfo &Inf) { if (Inf.Width.Max == 0) { Inf.Width.Min = -1; Inf.Width.Max = -1; } else { Inf.Height.Min = -1; Inf.Height.Max = -1; } return true; } int CalendarView::OnNotify(LViewI *v, LNotification n) { switch (v->GetId()) { case IDC_VSCROLL: { if (n.Type == LNotifyScrollBarCreate) { SetupScroll(); } Invalidate(); break; } } return 0; } void CalendarView::SelectDropTarget(LDateTime *start, LDateTime *end) { /* if (start && DropStart) { if (*start == *DropStart) { // the same return; } } DeleteObj(DropStart); DeleteObj(DropEnd); if (start) { DropStart = new LDateTime; if (DropStart) { *DropStart = *start; } if (end) { DropEnd = new LDateTime; if (DropEnd) { *DropEnd = *end; } } } Invalidate(); */ } void CalendarView::OnSelect(Calendar *c, bool Ctrl, bool Shift) { if (!Ctrl) { Selection.Length(0); } else { Selection.Delete(c); } if (c) { Selection.Add(c); Invalidate(&c->ViewPos); } } CalendarViewMode CalendarView::GetViewMode() { return Mode; } void CalendarView::SetupScroll() { SetScrollBars(false, Mode == CAL_VIEW_WEEK); if (VScroll) { int Page = (int)(Calendar::DayEnd - Calendar::DayStart); VScroll->SetRange(24); VScroll->SetPage(Page); VScroll->Value(Calendar::DayStart); } } void CalendarView::SetViewMode(CalendarViewMode m) { Mode = m; SetCursor(Cursor); Invalidate(); SetupScroll(); OnCursorChange(); } LDateTime &CalendarView::GetCursor() { return Cursor; } void CalendarView::SetCursor(LDateTime &c) { LDateTime Old = Cursor; Cursor = c; SendNotify(LNotifyCursorChanged); switch (Mode) { default: LAssert(0); break; /* case CAL_VIEW_DAY: { break; } case CAL_VIEW_WEEKDAY: { break; } */ case CAL_VIEW_WEEK: { Start = Cursor; int DayOfWeek = Start.DayOfWeek(); int Diff = FirstDayOfWeek - DayOfWeek; if (Diff > 0) Diff -= 7; Start.AddDays(Diff); First = Start; break; } case CAL_VIEW_MONTH: { First = Cursor; First.Day(1); Start = First; int DayOfWeek = Start.DayOfWeek(); int Diff = FirstDayOfWeek - DayOfWeek; if (Diff > 0) Diff -= 7; Start.AddDays(Diff); break; } case CAL_VIEW_YEAR: { YearView v(&Cursor); v.SetCursor(0, 0); First = v.Get(); Start = Cursor; Start.Day(1); Start.Month(1); break; } } bool YearCh = Old.Year() != Cursor.Year(); bool MthCh = Old.Month() != Cursor.Month() || YearCh; bool DayCh = Old.Day() != Cursor.Day() || MthCh; OnCursorChange(DayCh, MthCh, YearCh); } void CalendarView::OnSourceDelete(CalendarSource *s) { for (size_t i=0; i, bool> InCur; + for (unsigned i=0; iGetEvents(Start, End, Current); } LHashTbl, bool> InCur; for (unsigned i=0; iSource = 0; } + Current.Length(0); - for (unsigned i=0; iGetEvents(s, e, Current); - } + Current += Events; + Invalidate(); + }); LDateTime::GetDaylightSavingsInfo(Dst, s, &e); LWindow *Wnd = GetWindow(); if (Wnd) { char s[256]; sprintf_s(s, sizeof(s), "%s [%s %i]", LLoadString(IDS_CAL_VIEW), FullMonthNames[Cursor.Month()-1], Cursor.Year()); Wnd->Name(s); } } Invalidate(); } bool CalendarView::OnPrintPage(LPrintDC *pDC, int PageIndex) { LVariant Bx1, By1, Bx2, By2; LOptionsFile *Options = App->GetOptions(); LFontType FontType("Courier New", 8); FontType.GetSystemFont("small"); Bx1 = By1 = Bx2 = By2 = 1.0; // cm if (Options) { // read any options out.. #define GetMargin(opt, var) \ { LVariant v; if (Options->GetValue(opt, v)) var = v.CastDouble(); } GetMargin(OPT_MarginX1, Bx1); GetMargin(OPT_MarginY1, By1); GetMargin(OPT_MarginX2, Bx2); GetMargin(OPT_MarginY2, By2); } LAutoPtr ScreenFont = Font; if (pDC) { // setup device context double CmToInch = 0.393700787; auto ScreenDpi = LScreenDpi(); auto DcDpi = pDC->GetDpi(); double ScaleX = (double)ScreenDpi.x / DcDpi.x; double ScaleY = (double)ScreenDpi.y / DcDpi.y; // LRect c = GetClient(); PrintMargin.x1 = (int) (( (Bx1.CastDouble() * CmToInch) * DcDpi.x ) * ScaleX); PrintMargin.y1 = (int) (( (By1.CastDouble() * CmToInch) * DcDpi.y ) * ScaleY); PrintMargin.x2 = (int) (( pDC->X() - ((Bx2.CastDouble() * CmToInch) * DcDpi.x) ) * ScaleX); PrintMargin.y2 = (int) (( pDC->Y() - ((By2.CastDouble() * CmToInch) * DcDpi.y) ) * ScaleY); // setup font Font.Reset(FontType.Create(pDC)); if (Font) { Font->Colour(L_BLACK, L_WHITE); Font->Create(0, 0, pDC); LDisplayString ds(Font, " "); Font->TabSize(ds.X() * 8); OnPaint(pDC); } } Font = ScreenFont; return false; } int EventSorter(TimePeriod *a, TimePeriod *b) { return a->s.Compare(&b->s); } bool CalendarView::Overlap(LDateTime &Start, LDateTime &End, Calendar *a, Calendar *b) { if (a && b) { if (a->Overlap(b)) { LArray Ap, Bp; a->GetTimes(Start, End, Ap); b->GetTimes(Start, End, Bp); for (unsigned i=0; iFont->Colour(L_TEXT, L_MED); i->Font->Transparent(true); LDisplayString ds(i->Font, (char*)i->Txt); ds.Draw(pDC, i->x, i->y, &r); } void CalendarView::DrawSelectionBox(LSurface *pDC, LRect &r) { int Edge = 4; // int Width = 2; #ifdef WINDOWS int Op = pDC->Op(GDC_XOR); pDC->Colour(Rgba32(0xff, 0xff, 0xff, 0), 32); #else // Other platforms don't have a working XOR operator... so black is // a good default against their default White background. pDC->Colour(Rgb24(0, 0, 0), 24); #endif LRect p; // Top p.Set(r.x1, r.y1, r.x2, r.y1 + Edge - 1); PatternBox(pDC, p); // Bottom p.Set(r.x1, r.y2 - Edge + 1, r.x2, r.y2); PatternBox(pDC, p); // Left p.Set(r.x1, r.y1 + Edge, r.x1 + Edge - 1, r.y2 - Edge); PatternBox(pDC, p); // Right p.Set(r.x2 - Edge + 1, r.y1 + Edge, r.x2, r.y2 - Edge); PatternBox(pDC, p); // WriteDC("c:\\temp\\cal.bmp", pDC); #ifdef WINDOWS pDC->Op(Op); #endif } void CalendarView::OnPaint(LSurface *pDC) { #ifndef MAC // Mac is double buffered anyway LDoubleBuffer Buf(pDC); #endif LColour InMonth(L_WORKSPACE); LColour OutMonth = GdcMixColour(LColour(L_HIGH), LColour(L_WORKSPACE), 0.5); LColour CellEdge(0xc0, 0xc0, 0xc0); LSkinEngine *SkinEngine = LAppInst->SkinEngine; pDC->Colour(Rgb32(255, 255, 255), 32); pDC->Rectangle(); LRect c = GetClient(); c.Offset(-c.x1, -c.y1); float _Sx = 1.0; float _Sy = 1.0; if (pDC->IsPrint()) { c = PrintMargin; auto ScreenDpi = LScreenDpi(); auto DcDpi = pDC->GetDpi(); _Sx = (float)DcDpi.x / ScreenDpi.x; _Sy = (float)DcDpi.y / ScreenDpi.y; } float Scale = _Sx < _Sy ? _Sx : _Sy; SRect(c); Layout = c; if (Mode != CAL_VIEW_YEAR) { Title = c; Title.y2 = Title.y1 + Font->GetHeight() + (int)SY(6); Layout.y1 = Title.y2 + 1; } else { Title.ZOff(-1, -1); } for (auto &c: Current) { c.c->ViewPos.Empty(); } switch (Mode) { /* case CAL_VIEW_DAY: { break; } case CAL_VIEW_WEEKDAY: { break; } */ case CAL_VIEW_WEEK: { // Recalc day start/end int DayVisible = DayEnd - DayStart; DayStart = VScroll ? (int)VScroll->Value() : 6; DayEnd = DayStart + DayVisible; // Setup... LDateTime Dt = Start, Now; Dt.SetTime("0:0:0.0"); Now.SetNow(); LDisplayString ds(Font, "22:00p"); int TimeX = (int)((float)ds.X() + SX(10)); Layout.Set(TimeX, Title.y2 + 1, c.x2, c.y2); // d=0 is the hours column, d=1 is the first day (either sun or mon), etc... d=7 is last day for (int d=0; d<8; d++) { LDateTime Tomorrow = Dt; Tomorrow.AddDays(1); uint64 TodayTs, TomorrowTs; Dt.Get(TodayTs); Tomorrow.Get(TomorrowTs); // Heading int x1 = d ? TimeX + ((d-1) * (c.X()-TimeX) / 7) : 0; int x2 = d ? TimeX + ((d * (c.X()-TimeX) / 7) - 1) : TimeX - 1; LRect p( x1, Title.y1, x2, Title.y2); if (d) { int NameIdx = (FirstDayOfWeek+d-1) % 7; if (SkinEngine) { ColumnPaintInfo i = { Font, FullDayNames[NameIdx], (int)SX(2), (int)SY(3) }; LSkinState State; State.pScreen = pDC; State.Rect = p; State.View = this; SkinEngine->OnPaint_ListColumn(CalendarColumnPaint, &i, &State); } else { LWideBorder(pDC, p, DefaultRaisedEdge); Font->Colour(L_TEXT, L_MED); Font->Transparent(false); LDisplayString ds(Font, (char*)FullDayNames[NameIdx]); ds.Draw(pDC, p.x1 + (int)SX(2), p.y1 + (int)SY(2), &p); } } else { pDC->Colour(L_LOW); pDC->Rectangle(&p); } // Content area p.Set( x1, Title.y2 + 1, x2, c.y2); pDC->Colour(CellEdge); pDC->Line(p.x2, p.y1, p.x2, p.y2); int Divisions = DayEnd - DayStart; int DayOfWeek = Dt.DayOfWeek(); #define HourToY(hour) (p.y1 + (((hour)-(double)DayStart) * p.Y() / Divisions)) for (int h=DayStart; h<=DayEnd; h++) { LRect Hour( x1, (int)HourToY(h), x2, (int)HourToY(h+1)-1); LColour Back = DayOfWeek == 0 || DayOfWeek == 6 ? GdcMixColour(LColour(L_WORKSPACE), WEEKEND_TINT_COLOUR, WEEKEND_TINT_LEVEL) : OutMonth; if (DayOfWeek >= Calendar::WorkWeekStart && DayOfWeek <= Calendar::WorkWeekEnd && h >= Calendar::WorkDayStart && h < Calendar::WorkDayEnd) Back = InMonth; /* bool Select = Focus() && Cur->Day() == Dt.Day() && Cur->Hours() == h; if (Select) { Back = LC_FOCUS_SEL_BACK; } */ bool Today = Now.Day() == Dt.Day() && Now.Month() == Dt.Month() && Now.Year() == Dt.Year(); if (Today) { Back = GdcMixColour(Back, TODAY_TINT_COLOUR, TODAY_TINT_LEVEL); } if (d) { // Draw hourly blocks // Background pDC->Colour(Back); pDC->Rectangle(Hour.x1, Hour.y1, Hour.x2-1, Hour.y2-1); pDC->Colour(CellEdge); pDC->Line(Hour.x1, Hour.y2, Hour.x2, Hour.y2); // Date at the top.. if (h == DayStart) { char s[32]; Dt.GetDate(s, sizeof(s)); Font->Colour(/*Select ? LC_FOCUS_SEL_FORE :*/ L_LOW, L_MED); Font->Transparent(true); LDisplayString ds(Font, s); ds.Draw(pDC, Hour.x1 + 2, Hour.y1 + 2); } } else { // Draw times in the first column char s[32]; if (Now.GetFormat() & GDTF_24HOUR) sprintf_s(s, sizeof(s), "%i:00", h); else sprintf_s(s, sizeof(s), "%i:00%c", h == 0 ? 12 : h > 12 ? h - 12 : h, h >= 12 ? 'p' : 'a'); LRect Temp = Hour; LWideBorder(pDC, Temp, DefaultRaisedEdge); Font->Colour(L_TEXT, L_MED); Font->Transparent(false); LDisplayString ds(Font, s); ds.Draw(pDC, Temp.x1 + (int)SX(2), Temp.y1, &Temp); } } if (d) { // Draw events LArray All; for (uint32_t i=0; i EventArray; LArray Groups; for (uint32_t e=0; e 0) { // Check if it overlaps anything in the previous group EventArray *a = Groups[Groups.Length()-1]; for (uint32_t i=0; iLength(); i++) { TimePeriod *t = (*a)[i]; if (t->Overlap(*Ev)) { // It does... so add it. a->Add(Ev); Ev = 0; break; } } } if (Ev) { // Create new group EventArray *g = new EventArray; if (g) { g->Add(Ev); Groups.Add(g); } } } // Lay the groups of entries out and then paint them for (uint32_t g=0; gOnPaintView(pDC, Font, &Vp, &t); } } // Clean up the memory Groups.DeleteObjects(); for (unsigned i=0; i TomorrowTs) EndSec = (int)((TomorrowTs - TodayTs) / LDateTime::Second64Bit); else EndSec = (int)((rng.EndTs - TodayTs) / LDateTime::Second64Bit); int StartY = (int) HourToY((double)StartSec / LDateTime::HourLength); int EndY = (int) HourToY((double)EndSec / LDateTime::HourLength); /* printf("%f,%f - %i,%i - %i,%i\n", (double)StartSec / LDateTime::HourLength, (double)EndSec / LDateTime::HourLength, StartY, EndY, DayStart, Divisions); */ LRect r(p.x1 + 4, StartY, p.x2 - 4, EndY); DrawSelectionBox(pDC, r); } } // Increment date of day we're painting Dt = Tomorrow; } } break; } case CAL_VIEW_MONTH: { LDateTime *Cur = (DragStart.IsValid()) ? &DragStart : &Cursor; int ObjY = Font->GetHeight() + (int)SY(4); LDateTime i = Start, Now, Tomorrow; Now.SetNow(); i.SetTime("0:0:0.0"); Tomorrow = i; Tomorrow.AddDays(1); char Str[256]; for (int h=0; h<7; h++) { LRect p( Title.x1 + (h * Title.X() / MonthX), Title.y1, Title.x1 + (((h+1) * Title.X() / MonthX) - 1), Title.y2); int NameIdx = (h + FirstDayOfWeek) % 7; if (SkinEngine) { ColumnPaintInfo i = { Font, FullDayNames[NameIdx], (int)SX(2), (int)SY(3) }; LSkinState State; State.pScreen = pDC; State.Rect = p; State.View = this; SkinEngine->OnPaint_ListColumn(CalendarColumnPaint, &i, &State); } else { LWideBorder(pDC, p, DefaultRaisedEdge); Font->Colour(L_TEXT, L_MED); Font->Transparent(false); LDisplayString ds(Font, (char*)FullDayNames[NameIdx]); ds.Draw(pDC, p.x1 + (int)SX(2), p.y1 + (int)SY(2), &p); } } for (int y=0; yMonth(); bool Today = Now.Day() == i.Day() && Now.Month() == i.Month() && Now.Year() == i.Year(); if (i.Day() == Cur->Day() && i.Month() == Cur->Month() && i.Year() == Cur->Year()) { // Is cursor day Back = Focus() ? LColour(L_FOCUS_SEL_BACK) : GdcMixColour(LColour(L_FOCUS_SEL_BACK), LColour(L_WORKSPACE)); Font->Fore(L_FOCUS_SEL_FORE); } else { // normal day Back = (IsInMonth) ? InMonth : OutMonth; Font->Fore(L_TEXT); } if (DayOfWeek == 0 || DayOfWeek == 6) Back = GdcMixColour(Back, WEEKEND_TINT_COLOUR, WEEKEND_TINT_LEVEL); if (Today) Back = GdcMixColour(Back, TODAY_TINT_COLOUR, TODAY_TINT_LEVEL); int Edge = (int)SX(1); if (!pDC->IsPrint() || Back != LColour(L_WORKSPACE)) { pDC->Colour(Back); pDC->Rectangle(p.x1, p.y1, p.x2-Edge, p.y2-Edge); } pDC->Colour(CellEdge); pDC->Rectangle(p.x2-Edge+1, p.y1, p.x2, p.y2); pDC->Rectangle(p.x1, p.y2-Edge+1, p.x2-Edge, p.y2); if (pDC->IsPrint() && x == 0) { pDC->Rectangle(p.x1, p.y1, p.x1+Edge, p.y2-Edge); } Font->Transparent(true); Font->Back(Back); LDisplayString ds(Font, Str); ds.Draw(pDC, p.x1 + (int)SX(2), p.y1 + (int)SX(2)); LRect Clip = p; Clip.Inset(Edge, Edge); pDC->ClipRgn(&Clip); int CalY = ObjY + (int)SY(2); LArray All; uint32_t n; for (n=0; nGetTimes(i, Tomorrow, All); } All.Sort(EventSorter); for (n=0; nOnPaintView(pDC, Font, &Vp, &t); CalY += ObjY + (int)SY(2); } for (auto rng : Ranges) { if (rng.Overlap(i, Tomorrow)) { LRect Vp(p.x1 + (int)SX(3), p.y1 + CalY, p.x2 - (int)SX(4), p.y1 + CalY + ObjY); DrawSelectionBox(pDC, Vp); CalY += ObjY + (int)SY(2); break; } } pDC->ClipRgn(0); i = Tomorrow; Tomorrow.AddDays(1); } } break; } case CAL_VIEW_YEAR: { LDateTime *Cur = (DragStart.IsValid()) ? &DragStart : &Cursor; // int ObjY = Font->GetHeight() + (int)SY(2); LDateTime i = Start, Now, Tomorrow; YearView v(Cur); Now.SetNow(); i.Hours(0); i.Minutes(0); i.Seconds(0); i.Thousands(0); Tomorrow = i; Tomorrow.AddDays(1); Layout.x1 += (int)SX(BORDER_YEAR); int Fy = Font->GetHeight(); char Str[256]; for (int y=0; yTransparent(false); Font->Colour(L_BLACK, L_MED); LDisplayString ds(Font, (char*)ShortMonthNames[y]); ds.Draw(pDC, T.x1 + (int)SX(2), T.y1, &T); for (int x=0; xDay() && t.Month() == Cur->Month() && t.Year() == Cur->Year()) { // Is cursor day Back = Focus() ? LColour(L_FOCUS_SEL_BACK) : GdcMixColour(LColour(L_FOCUS_SEL_BACK), LColour(L_WORKSPACE)); Fore = LColour(L_FOCUS_SEL_FORE); } else { // Other day.. Fore = LColour(L_LOW); Back = v.IsMonth() ? InMonth : OutMonth; } // Weekend tint int Day = t.DayOfWeek(); if (Day == 0 || Day == 6) { Back = GdcMixColour(Back, WEEKEND_TINT_COLOUR, WEEKEND_TINT_LEVEL); } // Today tint.. if (v.IsMonth() && Now.Day() == t.Day() && Now.Month() == t.Month() && Now.Year() == t.Year()) { Back = GdcMixColour(Back, TODAY_TINT_COLOUR, TODAY_TINT_LEVEL); } // Fill cell background pDC->Colour(Back); pDC->Rectangle(p.x1, p.y1, p.x2-1, p.y2-1); pDC->Colour(CellEdge); pDC->Line(p.x2, p.y1, p.x2, p.y2); pDC->Line(p.x1, p.y2, p.x2, p.y2); // Draw day number if (v.IsMonth()) { Font->Transparent(true); Font->Colour(Fore, Back); sprintf_s(Str, sizeof(Str), "%i", t.Day()); LDisplayString ds(Font, Str); ds.Draw(pDC, p.x1+1, p.y1-1); } // Paint events LArray e; LDateTime Tomorrow(t); Tomorrow.AddDays(1); int Cy = p.y1 + Fy; if (v.IsMonth() && GetEventsBetween(e, t, Tomorrow)) { LRect Safe = p; Safe.y2--; for (uint32_t i=0; i Safe.y2) { c->ViewPos.ZOff(-1, -1); } else { LRect Vp(p.x1 + (int)SX(1), Cy, p.x2 - (int)SX(2), Cy + Fy); Vp.Bound(&Safe); c->OnPaintView(pDC, Font, &Vp, &e[i]); Cy += Fy + 1; } } } for (auto rng : Ranges) { if (rng.Overlap(t, Tomorrow)) { LRect Vp(p.x1 + (int)SX(1), Cy, p.x2 - (int)SX(2), Cy + Fy); DrawSelectionBox(pDC, Vp); Cy += Fy + 1; break; } } } } break; } default: { pDC->Colour(L_WHITE); pDC->Rectangle(); break; } } } bool CalendarView::OnKey(LKey &k) { switch (k.vkey) { case LK_ESCAPE: { if (IsCapturing() && k.Down()) { DragStart.Empty(); DragEnd.Empty(); Ranges.Length(0); DragMode = DragNone; Invalidate(); } return true; } default: { switch (k.c16) { case 'w': case 'W': { if (k.CtrlCmd() && k.Down() && GetWindow()) { GetWindow()->Quit(); return true; } break; } } } } /* if (k.c16 != 17 && k.Down()) { // Arrow behaviour CalViewArrow *Arrow = GetModeArrow(Mode, k); if (Arrow) { LDateTime c = Cursor; c.AddHours(Arrow->Hours); c.AddDays(Arrow->Days); c.AddMonths(Arrow->Months); c.AddMonths(Arrow->Years * 12); SetCursor(c); return true; } // Other commands switch (k.c16) { case LK_DELETE: { LVariant ConfirmDelete; App->GetOptions()->GetValue(OPT_ConfirmDelete, ConfirmDelete); if (!ConfirmDelete.CastInt32() || LgiMsg(this, LLoadString(IDS_DELETE_ASK), AppName, MB_YESNO) == IDYES) { OnDelete(); } return true; break; } } } */ return false; } void CalendarView::OnDelete() { Calendar *c; while ((c=Selection.First())) { c->OnDelete(); Selection.Delete(c); } } Calendar *CalendarView::CalendarAt(int x, int y) { switch (Mode) { default: LAssert(0); break; /* case CAL_VIEW_DAY: { break; } case CAL_VIEW_WEEKDAY: { break; } */ case CAL_VIEW_WEEK: case CAL_VIEW_MONTH: case CAL_VIEW_YEAR: { if (Layout.Overlap(x, y)) { for (uint32_t i=0; iViewPos.Overlap(x, y)) { return Current[i].c; } } } break; } } return 0; } LDateTime *CalendarView::TimeAt(int x, int y, int SnapMinutes, LPoint *Cell) { LPoint cell; if (!Cell) Cell = &cell; Cell->x = -1; Cell->y = -1; switch (Mode) { default: break; /* case CAL_VIEW_DAY: { break; } case CAL_VIEW_WEEKDAY: { break; } */ case CAL_VIEW_WEEK: { if (Layout.Overlap(x, y)) { LDateTime *Day = new LDateTime; if (Day) { int64 Page = (int)(Calendar::DayEnd - Calendar::DayStart); int FirstHr = 0; if (VScroll) { FirstHr = (int) VScroll->Value(); Page = VScroll->Page(); } Cell->x = ((x - Layout.x1) * 7) / Layout.X(); // day in week int MinuteOffset = ((y - Layout.y1) * (int)Page * 60) / Layout.Y(); Cell->y = ((y - Layout.y1) * (int)Page) / Layout.Y(); // hour of day LAssert(MinuteOffset / 60 == Cell->y); int Minutes = (FirstHr * 60) + MinuteOffset; if (SnapMinutes) { int Snap = Minutes % SnapMinutes; if (Snap) { Minutes -= Snap; } } *Day = Start; Day->AddDays(Cell->x); Day->Hours(Minutes / 60); Day->Minutes(Minutes % 60); Day->Seconds(0); Day->Thousands(0); return Day; } } break; } case CAL_VIEW_MONTH: { if (Layout.Overlap(x, y)) { LDateTime *Day = new LDateTime; if (Day) { Cell->x = ((x - Layout.x1) * MonthX) / Layout.X(); Cell->y = ((y - Layout.y1) * MonthY) / Layout.Y(); *Day = Start; Day->AddDays( (Cell->y * MonthX) + Cell->x ); Day->SetTime("0:0:0"); return Day; } } break; } case CAL_VIEW_YEAR: { if (Layout.Overlap(x, y)) { LDateTime *Day = new LDateTime; if (Day) { YearView v(&Cursor); Cell->x = (x - Layout.x1) * v.X() / Layout.X(); Cell->y = (y - Layout.y1) * v.Y() / Layout.Y(); v.SetCursor(Cell->x, Cell->y); *Day = v.Get(); Day->SetTime("0:0:0"); return Day; } } break; } } return 0; } LDateTime::GDstInfo *CalendarView::GetDstForDate(LDateTime t) { uint64 ts = t; for (uint32_t i=0; i= Prev && ts < Next) { return &Dst[i]; } } else if (ts > Dst[i].UtcTimeStamp) { return &Dst[i]; } } return 0; } bool CalendarView::HitTest(int x, int y, EventDragMode &mode, Calendar *&event) { if (!Layout.Overlap(x, y)) return false; for (uint32_t i=0; iViewPos.First(); r; r = c->ViewPos.Next()) { if (x >= r->x1 && x <= r->x2) { if (abs(y-r->y1) < THRESHOLD_EDGE) { event = c; mode = DragMoveStart; return true; } if (abs(y-r->y2) < THRESHOLD_EDGE) { event = c; mode = DragMoveEnd; return true; } } } } if (c->ViewPos.Overlap(x, y)) { event = c; mode = DragMoveSelection; return true; } } return false; } Calendar *CalendarView::NewEvent(LDateTime &dtStart, LDateTime &dtEnd) { CalendarSource *Src = FolderCalendarSource::GetCreateIn(); if (!Src) return NULL; Calendar *c = Src->NewEvent(); if (!c) return NULL; LDateTime Start, End; if (dtStart < dtEnd) { Start = dtStart; End = dtEnd; } else { Start = dtEnd; End = dtStart; } LDateTime n = Start; LDateTime::GDstInfo *CurDst = GetDstForDate(Start); if (CurDst) n.SetTimeZone(CurDst->Offset, false); char s[64]; sprintf_s(s, sizeof(s), "%+.1f", (double)n.GetTimeZone() / 60.0); c->SetField(FIELD_CAL_TIMEZONE, s); Start.ToUtc(true); c->SetField(FIELD_CAL_START_UTC, Start); End.ToUtc(true); c->SetField(FIELD_CAL_END_UTC, End); LgiTrace("Start=%s, End=%s\n", Start.Get().Get(), End.Get().Get()); c->OnCreate(); return c; } void CalendarView::OnMouseClick(LMouse &m) { EventDragMode HitMode = DragNone; Calendar *c = NULL; HitTest(m.x, m.y, HitMode, c); bool AlreadySelected = (c) ? Selection.HasItem(c) : false; LAutoPtr Hit(TimeAt(m.x, m.y, SnapMinutes)); /* if (Hit) printf("Hit=%s\n", Hit->Get().Get()); */ if (m.IsContextMenu()) { Focus(true); // Select the item if not already selected when asking for a context menu. if (c && !Selection.HasItem(c)) { Selection.Add(c); Invalidate(&c->ViewPos); } if (!c) { char sHit[64] = ""; if (Hit) Hit->GetTime(sHit, sizeof(sHit)); LString NewMsg; NewMsg.Printf("New event at %s", sHit); LSubMenu s; s.AppendItem(NewMsg, IDM_NEW_EVENT); LSubMenu *snap = s.AppendSub("Snap"); if (snap) { LMenuItem *it = snap->AppendItem("15 minutes", IDM_15MIN); if (it && SnapMinutes == 15) it->Checked(true); it = snap->AppendItem("30 minutes", IDM_30MIN); if (it && SnapMinutes == 30) it->Checked(true); it = snap->AppendItem("1 hour", IDM_1HR); if (it && SnapMinutes == 60) it->Checked(true); } m.ToScreen(); int Cmd = s.Float(this, m); switch (Cmd) { case IDM_NEW_EVENT: { LDateTime End = *Hit; End.AddHours(1); Calendar *ev = NewEvent(*Hit, End); if (ev) ev->DoUI(); break; } case IDM_15MIN: { SnapMinutes = 15; break; } case IDM_30MIN: { SnapMinutes = 30; break; } case IDM_1HR: { SnapMinutes = 60; break; } } } } else { Capture(m.Down()); if (m.Down()) { Focus(true); DragEvent = NULL; if (m.Left()) { Ranges.Length(0); if (!AlreadySelected) { OnSelect(c, m.Ctrl(), m.Shift()); } ClickPt.x = m.x; ClickPt.y = m.y; if (Hit) { DragStart = *Hit; if (HitMode == DragMoveStart) { DragMode = HitMode; DragStart = *c->GetObject()->GetDate(FIELD_CAL_END_UTC); DragEnd = *c->GetObject()->GetDate(FIELD_CAL_START_UTC); DragStart.ToLocal(true); DragEnd.ToLocal(true); DragEvent = c; TsRange &r = Ranges.New(); DragStart.Get(r.StartTs); DragEnd.Get(r.EndTs); } else if (HitMode == DragMoveEnd) { DragMode = HitMode; DragStart = *c->GetObject()->GetDate(FIELD_CAL_START_UTC); DragEnd = *c->GetObject()->GetDate(FIELD_CAL_END_UTC); DragStart.ToLocal(true); DragEnd.ToLocal(true); DragEvent = c; TsRange &r = Ranges.New(); DragStart.Get(r.StartTs); DragEnd.Get(r.EndTs); } else if (Selection.Length()) { DragMode = DragMoveSelection; DragEnd = *Hit; for (unsigned i=0; iGetObject()->GetDate(FIELD_CAL_START_UTC); dt.ToLocal(true); dt.Get(r.StartTs); dt = *s->GetObject()->GetDate(FIELD_CAL_END_UTC); if (dt.IsValid()) { dt.ToLocal(true); dt.Get(r.EndTs); } else { r.EndTs = r.StartTs + ((uint64)60 * 60 * LDateTime::Second64Bit); } // Does this new range overlap any existing range? for (unsigned n=0; n %s\n", DragStart.Get().Get(), DragEnd.Get().Get()); TsRange &r = Ranges.New(); DragStart.Get(r.StartTs); DragEnd.Get(r.EndTs); } } else { DragStart.Empty(); DragEnd.Empty(); } Invalidate(); } } else // up { if (DragStart.IsValid() && DragEnd.IsValid()) { switch (DragMode) { case DragNewEvent: { // New event... // printf("DragStart=%s, DragEnd=%s\n", DragStart.Get().Get(), DragEnd.Get().Get()); c = NewEvent(DragStart, DragEnd); if (c) c->DoUI(); OnContentsChanged(NULL); break; } case DragMoveSelection: { // Adjust the times of the selection by the offset. int64 TsOffset = DragEnd.Ts() - DragStart.Ts(); if (TsOffset != 0) { // TsOffset = 0; for (unsigned i=0; iGetObject(); LDateTime start_dt = *o->GetDate(FIELD_CAL_START_UTC); start_dt.Set(start_dt.Ts() + TsOffset); o->SetDate(FIELD_CAL_START_UTC, &start_dt); LDateTime end_dt = *o->GetDate(FIELD_CAL_END_UTC); end_dt.Set(end_dt.Ts() + TsOffset); o->SetDate(FIELD_CAL_END_UTC, &end_dt); o->SetInt(FIELD_STATUS, Store3Delayed); s->SetDirty(); } OnContentsChanged(NULL); } break; } case DragMoveStart: { if (DragEvent) { if (DragEnd > DragStart) { LDateTime dt = DragStart; dt.ToUtc(true); DragEvent->GetObject()->SetDate(FIELD_CAL_START_UTC, &dt); dt = DragEnd; dt.ToUtc(true); DragEvent->GetObject()->SetDate(FIELD_CAL_END_UTC, &dt); } else { LDateTime dt = DragEnd; dt.ToUtc(true); DragEvent->GetObject()->SetDate(FIELD_CAL_START_UTC, &dt); } DragEvent->SetDirty(); OnContentsChanged(NULL); } break; } case DragMoveEnd: { if (DragEvent) { if (DragEnd < DragStart) { LDateTime dt = DragEnd; dt.ToUtc(true); DragEvent->GetObject()->SetDate(FIELD_CAL_START_UTC, &dt); dt = DragStart; dt.ToUtc(true); DragEvent->GetObject()->SetDate(FIELD_CAL_END_UTC, &dt); } else { LDateTime dt = DragEnd; dt.ToUtc(true); DragEvent->GetObject()->SetDate(FIELD_CAL_END_UTC, &dt); } DragEvent->SetDirty(); OnContentsChanged(NULL); } break; } default: break; } } if (AlreadySelected) { OnSelect(c, m.Ctrl(), m.Shift()); } if (DragStart.IsValid() || Ranges.Length()) { Ranges.Length(0); DragStart.Empty(); DragEnd.Empty(); Invalidate(); } DragEvent = NULL; } } if (c) { c->OnMouseClick(m); } } void CalendarView::OnMouseMove(LMouse &m) { if (IsCapturing()) { LAutoPtr Hit(TimeAt(m.x, m.y, SnapMinutes)); if ( Hit && ( abs(m.x - ClickPt.x) > 4 || abs(m.y - ClickPt.y) > 4 ) ) { #if 1 switch (DragMode) { default: break; case DragMoveStart: case DragMoveEnd: case DragNewEvent: { LDateTime End = *Hit; if (End >= DragStart) End.AddMinutes(SnapMinutes); if (End != DragEnd) { DragEnd = End; if (DragStart < DragEnd) { DragStart.Get(Ranges[0].StartTs); DragEnd.Get(Ranges[0].EndTs); } else { DragStart.Get(Ranges[0].EndTs); DragEnd.Get(Ranges[0].StartTs); } Invalidate(); } break; } case DragMoveSelection: { DragEnd = *Hit; int64 TsOffset = DragEnd.Ts() - DragStart.Ts(); if (LastTsOffset != TsOffset) { LastTsOffset = TsOffset; Ranges.Length(0); for (unsigned i=0; iGetObject()->GetDate(FIELD_CAL_START_UTC); dt.ToLocal(true); dt.Get(r.StartTs); dt = *s->GetObject()->GetDate(FIELD_CAL_END_UTC); if (dt.IsValid()) { dt.ToLocal(true); dt.Get(r.EndTs); } else r.EndTs = r.StartTs + ((uint64)60 * 60 * LDateTime::Second64Bit); r.StartTs += TsOffset; r.EndTs += TsOffset; // Does this new range overlap any existing range? for (unsigned n=0; n &Data, LPoint Pt, int KeyState) { for (unsigned di=0; di in(new LFile); auto type = LGetFileMimeType(f); if (in->Open(f, O_READ)) { auto c = First->NewEvent(); if (c) c->Import(c->AutoCast(in), type); } } } else LgiTrace("%s:%i - Do data sources?\n", _FL); } else if (_stricmp(dd.Format, ScribeCalendarObject) == 0) { if (dd.Data.Length() == 0) continue; LVariant *v = &dd.Data[0]; if (v->Type == GV_BINARY && v->Value.Binary.Length >= sizeof(NativeInt)*2 && DragStart.IsValid()) { NativeInt *d = (NativeInt*) v->Value.Binary.Data; if (d[0] == MAGIC_CALENDAR && d[1] > 0) { char Str[32]; if (Mode == CAL_VIEW_WEEK || Mode == CAL_VIEW_DAY) { DragStart.Get(Str, sizeof(Str)); } else { DragStart.GetDate(Str, sizeof(Str)); } Calendar **c = (Calendar**) (d + 2); for (int i=0; iGetField(FIELD_CAL_START_UTC, s)) { bool HasEnd = c[i]->GetField(FIELD_CAL_END_UTC, e); LDateTime Diff; if (HasEnd) Diff = e - s; else Diff.Hours(1); s.Set(Str); c[i]->SetField(FIELD_CAL_START_UTC, s); if (HasEnd) { e = s + Diff; c[i]->SetField(FIELD_CAL_END_UTC, e); } c[i]->Save(); } } SetCursor(DragStart); } } } } SelectDropTarget(); return 0; } void CalendarView::OnDragInit(bool Success) { } char *CalendarView::TypeOf() { return 0; } bool CalendarView::GetData(LArray &Data) { if (Selection.Length() <= 0) return false; bool Status = false; for (unsigned di=0; di(s); if (t) { Status |= t->GetDropFiles(Files); } } if (Status) { LMouse m; GetMouse(m, true); Status |= CreateFileDrop(&dd, m, Files); } } else if (_stricmp(dd.Format, ScribeCalendarObject) == 0) { ssize_t Size = (2 + Selection.Length()) * sizeof(NativeInt); LArray d; if (d.Length(Size)) { d[0] = MAGIC_CALENDAR; d[1] = Selection.Length(); int n=0; Calendar **l = (Calendar**) (&d[2]); for (unsigned i=0; i 0; } class CalendarViewPrint : public LPrintEvents { CalendarView *cv; public: CalendarViewPrint(CalendarView *v) { cv = v; } bool OnPrintPage(LPrintDC *pDC, int PageIndex) { return cv->OnPrintPage(pDC, PageIndex); } }; ////////////////////////////////////////////////////////////////////////////// LMonthView::LMonthView(int Id, LDateTime *n, CalendarView *calView) : MonthView(n) { FirstDayOfWeek = CalendarView::FirstDayOfWeek; SetId(Id); rTitle.ZOff(-1, -1); rCells.ZOff(-1, -1); Cell = 1; CalView = calView; Set(n); } void LMonthView::OnCellClick(int Cx, int Cy) { SetCursor(Cx, Cy); Invalidate(); SendNotify(LNotifyCursorChanged); } void LMonthView::SeekMonth(int Dir) { LDateTime t = Cursor; t.AddMonths(Dir); Set(&t); Invalidate(); SendNotify(LNotifyCursorChanged); } void LMonthView::OnMouseClick(LMouse &m) { if (m.IsContextMenu()) { } else if (m.Left() && m.Down()) { if (rCells.Overlap(m.x, m.y)) { int x = (m.x - rCells.x1) / Cell; int y = (m.y - rCells.y1) / Cell; OnCellClick(x, y); } else if (rLeft.Overlap(m.x, m.y)) { SeekMonth(-1); } else if (rRight.Overlap(m.x, m.y)) { SeekMonth(1); } } } void LMonthView::OnPaint(LSurface *pDC) { pDC->Colour(L_WORKSPACE); pDC->Rectangle(); Cell = LView::X() / MonthView::X(); LSysFont->Colour(L_TEXT, L_WORKSPACE); LSysFont->Transparent(false); LRect Client = GetClient(); LDisplayString n(LSysBold, Title()); LSysBold->Colour(L_TEXT, L_WORKSPACE); LSysBold->Transparent(true); n.Draw(pDC, 6, 0); rTitle.ZOff(Client.X()-1, n.Y()*3/2-1); rRight = rTitle; rRight.x1 = rRight.x2 - LSysFont->GetHeight() + 1; rLeft = rRight; rLeft.Offset(-rLeft.Y(), 0); rCells.ZOff(Cell * MonthView::X(), Cell * MonthView::Y()); rCells.Offset(0, rTitle.Y()); LDisplayString DsLeft(LSysFont, "<"); DsLeft.Draw(pDC, rLeft.x1, rLeft.y1); LDisplayString DsRight(LSysFont, ">"); DsRight.Draw(pDC, rRight.x1, rRight.y1); LDateTime t = Start; auto Mode = CalView->GetViewMode(); auto CursorPos = MonthView::GetCursor(); for (int y=0; yColour(L_FOCUS_SEL_FORE, L_FOCUS_SEL_BACK); else { LColour Fore, Back; if (InMonth) { Fore = LColour(L_TEXT); Back.Rgb(0xf7, 0xf7, 0xf7); } else { Fore.Rgb(192, 192, 192); Back = LColour(L_WORKSPACE); } if (Mode == CAL_VIEW_WEEK && y == CursorPos.y) { Back = Back.Mix(LColour(L_FOCUS_SEL_BACK), 0.1f); } LSysFont->Colour(Fore, Back); } LRect r; r.ZOff(Cell-2, Cell-2); r.Offset(x*Cell, y*Cell+rCells.y1); int Cx = Cell - Ds.X(); int Cy = Cell - Ds.Y(); Ds.Draw(pDC, r.x1+(Cx>>1), r.y1+(Cy>>1), &r); if (!t.AddDays(1)) { LAssert(!"Add days failed."); break; } } } } ////////////////////////////////////////////////////////////////////////////// LArray CalendarViewWindows; CalendarViewWnd::CalendarViewWnd(ScribeFolder *folder) { CalendarViewWindows.Add(this); App = folder ? folder->App : 0; Cv = 0; Split = 0; Todo = 0; HorBox = NULL; VerBox = NULL; CalLst = NULL; MonthV = NULL; Name("Calendar View"); if (!SerializeState(App->GetOptions(), OPT_CalendarViewPos, true)) { LRect p(0, 0, 600, 500); SetPos(p); MoveToCenter(); } LDateTime Now; Now.SetNow(); OnOptionsChange(); #if !defined(WINDOWS) SetIcon("_cal.png"); #endif auto ToolBar = folder->App->LoadToolbar(this, folder->App->GetResourceFile(ResToolbarFile), folder->App->GetToolbarImgList()); if (ToolBar) { AddView(ToolBar); ToolBar->AppendButton(RemoveAmp(LLoadString(IDS_WEEK)), IDM_CAL_WEEK, TBT_RADIO, true, IMG_CAL_WEEK); ToolBar->AppendButton(RemoveAmp(LLoadString(IDS_MONTH)), IDM_CAL_MONTH, TBT_RADIO, true, IMG_CAL_MONTH); ToolBar->AppendButton(RemoveAmp(LLoadString(IDS_YEAR)), IDM_CAL_YEAR, TBT_RADIO, true, IMG_CAL_YEAR); ToolBar->AppendSeparator(); ToolBar->AppendButton(0, IDM_PREV, TBT_PUSH, true, IMG_CAL_PREV); ToolBar->AppendButton(0, IDM_BACK, TBT_PUSH, true, IMG_CAL_BACK); ToolBar->AppendButton(LLoadString(IDS_TODAY), IDM_TODAY, TBT_PUSH, true, IMG_CAL_TODAY); ToolBar->AppendButton(0, IDM_FORWARD, TBT_PUSH, true, IMG_CAL_FORWARD); ToolBar->AppendButton(0, IDM_NEXT, TBT_PUSH, true, IMG_CAL_NEXT); ToolBar->AppendSeparator(); ToolBar->AppendButton(RemoveAmp(LLoadString(IDS_TODO)), IDM_TODO, TBT_TOGGLE, true, IMG_CAL_TODO); // ToolBar->AppendButton(RemoveAmp(LLoadString(IDS_CONFIGURE)), IDM_CONFIG, TBT_PUSH, true, IMG_CAL_CONFIG); ToolBar->AppendButton(RemoveAmp(LLoadString(IDS_PRINT)), IDM_PRINT, TBT_PUSH, true, IMG_PRINT); ToolBar->AppendButton(RemoveAmp(LLoadString(IDS_HELP)), IDM_HELP, TBT_PUSH, true, IMG_HELP); } // Month control #if defined(WINDOWS) int ColPixels = 160; #else int ColPixels = 180; #endif LCss::Len ColPx(LCss::LenPx, (float)ColPixels); LCss::Len Auto("auto"); LCss::Len Pad("10px"); Cv = new CalendarView(folder, IDC_CALENDAR, NULL, "Calendar View"); AddView(HorBox = new LBox); HorBox->AddView(VerBox = new LBox); HorBox->GetCss(true)->BackgroundColor(LColour(L_WORKSPACE)); VerBox->SetVertical(true); VerBox->GetCss(true)->Width(ColPx); VerBox->GetCss(true)->BackgroundColor(LColour(L_WORKSPACE)); VerBox->AddView(MonthV = new LMonthView(IDC_MONTH_VIEW, &Now, Cv)); MonthV->GetCss(true)->Height(ColPx); LRect r(0, 0, ColPixels-1, ColPixels-1); MonthV->SetPos(r); // c->Padding(Pad); // List of calendar sources... VerBox->AddView(CalLst = new LList(IDC_LIST, 0, 0, 100, 100, "Calendar Sources")); CalLst->AddColumn("x", 20); CalLst->AddColumn("Calendar", 150); // Main layout view HorBox->AddView(Cv); if (Cv) { Cv->OnCursorChange(false, true, false); Cv->Visible(true); switch (Cv->GetViewMode()) { default: break; case CAL_VIEW_WEEK: SetCtrlValue(IDM_CAL_WEEK, 1); break; case CAL_VIEW_MONTH: SetCtrlValue(IDM_CAL_MONTH, 1); break; case CAL_VIEW_YEAR: SetCtrlValue(IDM_CAL_YEAR, 1); break; } } if (Cv && CalLst) { for (unsigned i=0; i(s); CalLst->Insert(Li); bool IsCreateIn = FolderCalendarSource::GetCreateIn() == s; Li->Select(IsCreateIn); } } #if WINNATIVE CreateClassW32("Calendar", LoadIcon(LProcessInst(), MAKEINTRESOURCE(IDI_CALENDER))); #endif if (Attach(0)) { AttachChildren(); Visible(true); LVariant ViewTodo; App->GetOptions()->GetValue(OPT_CalendarViewTodo, ViewTodo); SetCtrlValue(IDM_TODO, ViewTodo.CastInt32()); if (ViewTodo.CastInt32()) { // Layout(); } } } CalendarViewWnd::~CalendarViewWnd() { if (CalLst) CalLst->RemoveAll(); // The CalendarView owns the list items. SerializeState(App->GetOptions(), OPT_CalendarViewPos, false); LVariant s; App->GetOptions()->SetValue(OPT_CalendarViewTodo, s = (int)GetCtrlValue(IDM_TODO)); CalendarViewWindows.Delete(this); } void CalendarViewWnd::OptionsChange() { LVariant v; int FirstDayOfWeek = 0; if (App->GetOptions()->GetValue(OPT_CalendarFirstDayOfWeek, v)) FirstDayOfWeek = v.CastInt32(); if (MonthV) { MonthV->FirstDayOfWeek = CalendarView::FirstDayOfWeek; MonthV->Invalidate(); } } void CalendarViewWnd::OnOptionsChange() { if (!CalendarView::App && CalendarViewWindows.Length() > 0) { CalendarView::App = CalendarViewWindows[0]->App; } CalendarView::OnOptionsChange(); for (auto w: CalendarViewWindows) w->OptionsChange(); } bool CalendarViewWnd::OnKey(LKey &k) { switch (k.vkey) { case 'w': case 'W': { if (k.CtrlCmd() && k.Down()) { Quit(); return true; } break; } } return LWindow::OnKey(k); } int CalendarViewWnd::OnCommand(int Cmd, int Event, OsView WndHandle) { CalViewArrow *Va = 0; switch (Cmd) { case IDM_CAL_DAY: { Cv->SetViewMode(CAL_VIEW_DAY); break; } case IDM_CAL_WEEK: { Cv->SetViewMode(CAL_VIEW_WEEK); break; } case IDM_CAL_MONTH: { Cv->SetViewMode(CAL_VIEW_MONTH); break; } case IDM_CAL_YEAR: { Cv->SetViewMode(CAL_VIEW_YEAR); break; } case IDM_PREV: { LKey k; k.c16 = LK_PAGEUP; k.Flags = LGI_EF_CTRL; Va = GetModeArrow(Cv->GetViewMode(), k); break; } case IDM_BACK: { LKey k; k.c16 = LK_PAGEUP; Va = GetModeArrow(Cv->GetViewMode(), k); break; } case IDM_TODAY: { LDateTime Dt; Dt.SetNow(); Dt.Minutes(0); Dt.Seconds(0); Dt.Thousands(0); Cv->SetCursor(Dt); break; } case IDM_FORWARD: { LKey k; k.c16 = LK_PAGEDOWN; Va = GetModeArrow(Cv->GetViewMode(), k); break; } case IDM_NEXT: { LKey k; k.c16 = LK_PAGEDOWN; k.Flags = LGI_EF_CTRL; Va = GetModeArrow(Cv->GetViewMode(), k); break; } case IDM_PRINT: { auto *Printer = Cv && App ? App->GetPrinter() : NULL; if (Printer) { CalendarViewPrint Cvp(Cv); Printer->Print(&Cvp, NULL, "Scribe Calendar", -1, this); } break; } case IDM_HELP: { App->LaunchHelp("calendar.html"); break; } } if (Va) { LDateTime Cursor = Cv->GetCursor(); Cursor.AddHours(Va->Hours); Cursor.AddDays(Va->Days); Cursor.AddMonths(Va->Months); Cursor.AddMonths(Va->Years * 12); Cv->SetCursor(Cursor); } return 0; } LMessage::Result CalendarViewWnd::OnEvent(LMessage *m) { switch (m->Msg()) { case M_CHANGE: { if (m->A() == IDC_LIST) { // One of the calendar source's has changed... // Check it's still in our list and 'valid' CalendarSource *s = (CalendarSource*)m->B(); LArray All; CalLst->GetAll(All); if (All.IndexOf(s) >= 0) Cv->OnContentsChanged(s); } break; } #ifdef WIN32 case WM_CLOSE: { Quit(); return 0; } #endif } return LWindow::OnEvent(m); } LString CalendarViewWnd::LoadString(int id) { return LString(LLoadString(id)).Replace("&",""); } LString CalendarViewWnd::UnusedKey() { LString Key; // Find an unused index for the new source... while (true) { Key.Printf("%s.Source-%i", OPT_CalendarSources, LRand(1000)); if (App->GetOptions()->LockTag(Key, _FL)) App->GetOptions()->Unlock(); else break; } return Key; } int CalendarViewWnd::OnNotify(LViewI *c, LNotification n) { static bool Processing = false; if (Processing) return 0; Processing = true; switch (c->GetId()) { case IDC_CALENDAR: { if (Cv && MonthV && n.Type == LNotifyCursorChanged) { LDateTime t = Cv->GetCursor(); MonthV->Set(&t); MonthV->Invalidate(); } break; } case IDC_MONTH_VIEW: { if (Cv && MonthV && n.Type == LNotifyCursorChanged) { LDateTime t; t = MonthV->Get(); Cv->SetCursor(t); } break; } case IDC_LIST: { if (!CalLst) break; switch (n.Type) { default: break; case LNotifyValueChanged: { if (Cv) { FolderCalendarSource *Src = dynamic_cast(CalLst->GetSelected()); if (Src) Cv->OnContentsChanged(Src); } break; } case LNotifyItemContextMenu: { LMouse m; GetMouse(m); m.ToScreen(); LListItem *Sel = CalLst->GetSelected(); LSubMenu s; if (Sel) { LSubMenu *ColMenu = s.AppendSub("Colour"); if (ColMenu) { BuildMarkMenu( ColMenu, MS_None, 0); } } s.AppendItem(LLoadString(IDS_ADD_LOCAL_CAL_FOLDER), IDM_ADD_LOCAL_CAL); s.AppendItem(LLoadString(IDS_ADD_CAL_URL), IDM_ADD_CAL_URL); s.AppendSeparator(); s.AppendItem(LoadString(IDS_EDIT), IDM_EDIT, Sel != NULL); s.AppendItem(LoadString(IDS_DELETE), IDM_DELETE, Sel != NULL); int Id = s.Float(this, m); switch (Id) { case IDM_ADD_LOCAL_CAL: { auto Dlg = new FolderDlg(this, App, MAGIC_CALENDAR); Dlg->DoModal([this, Dlg](auto dlg, auto ctrlId) { if (ctrlId) { auto Key = UnusedKey(); auto Parts = Key.SplitDelimit("."); // Create the source... FolderCalendarSource *cs = new FolderCalendarSource(App, Parts.Last()); if (cs) { cs->SetPath(Dlg->Get()); cs->SetColour(CalendarSource::FindUnusedColour()); CalLst->Insert(cs); // CalLst doesn't own the ptr cs->Write(); } App->SaveOptions(); } delete dlg; }); break; } case IDM_ADD_CAL_URL: { auto dlg = new LInput(this); dlg->DoModal([this, dlg](auto dialog, auto ctrlId) { if (ctrlId) { auto Key = UnusedKey(); auto Parts = Key.SplitDelimit("."); auto Url = dlg->GetStr(); // Create the source... RemoteCalendarSource *cs = new RemoteCalendarSource(App, Parts.Last()); if (cs) { cs->SetColour(CalendarSource::FindUnusedColour()); cs->SetUri(Url); CalLst->Insert(cs); // CalLst doesn't own the ptr cs->Write(); } App->SaveOptions(); } delete dialog; }); break; } case IDM_EDIT: { CalendarSource *Src = dynamic_cast(Sel); if (!Src) break; Src->EditPath(this, Cv); break; } case IDM_DELETE: { CalendarSource *Src = dynamic_cast(Sel); if (Cv && Src) { auto *Lst = Src->LListItem::GetList(); if (Lst) Lst->Remove(Src); Cv->DeleteSource(Src); } break; } default: { int Idx = Id - IDM_MARK_BASE; if (Idx >= 0 && Idx < CountOf(MarkColours32)) { LColour Mc(MarkColours32[Idx], 32); CalendarSource *Src = dynamic_cast(Sel); if (Src) { Src->SetColour(Mc); Src->Write(); } } break; } } break; } } break; } case IDC_TODO: { if (Todo && n.Type == LNotifyItemColumnClicked) { int Col = 0; LMouse m; if (Todo->GetColumnClickInfo(Col, m)) { int Sort = 0; for (int i=0; iGetColumns(); i++) { LItemColumn *c = Todo->ColumnAt(i); if (c) { if (i == Col) { if (c->Mark() == GLI_MARK_DOWN_ARROW) { c->Mark(GLI_MARK_UP_ARROW); Sort = -(i + 1); } else { c->Mark(GLI_MARK_DOWN_ARROW); Sort = i + 1; } } else { c->Mark(GLI_MARK_NONE); } } } Todo->Sort(TodoCompare, Sort); } } break; } } Processing = false; return 0; } ////////////////////////////////////////////////////////////////////////////// void OpenCalender(ScribeFolder *folder) { if (!CalendarView::CalendarViews.Length()) { new CalendarViewWnd(folder); } } void CalendarSource::FolderDelete(ScribeFolder *f) { for (auto s: AllSources) s->OnFolderDelete(f); } LColour CalendarSource::FindUnusedColour() { // MarkColours32 LArray Used; Used.Length(IDM_MARK_MAX); for (auto &s: AllSources) { auto c = s->GetColour(); uint32_t c32 = c.c32(); for (int i=0; i CalendarSource::AllSources; LString CalendarSource::GetKey() { LString k; if (Id) k.Printf("%s.%s", OPT_CalendarSources, Id.Get()); return k; } LColour CalendarSource::GetColour() { return Colour; } ///////////////////////////////////////////////////////////////////////////////////// FolderCalendarSource::FolderCalendarSource(ScribeWnd *a, const char *id) { Id = id; App = a; Folder = NULL; } FolderCalendarSource::~FolderCalendarSource() { } void FolderCalendarSource::OnPulse() { if (!Folder) { Folder = App->GetFolder(Path); if (Folder) OnChange(false); } } void FolderCalendarSource::OnFolderDelete(ScribeFolder *f) { if (Folder == f) { Folder = NULL; OnChange(true); } } void FolderCalendarSource::SetColour(LColour c) { Colour = c; OnChange(false); } bool FolderCalendarSource::Delete() { LString k = GetKey(); bool r = App->GetOptions()->DeleteTag(k); if (r) { if (Folder) App->RemoveThingSrc(Folder); App->SaveOptions(); } else LAssert(!"Delete failed."); return r; } void FolderCalendarSource::SetPath(const char *p) { Path = p; Folder = App->GetFolder(Path); if (Path) { Write(); OnChange(false); } } void FolderCalendarSource::OnChange(bool IsDelete) { Update(); if (!GetList()) return; auto w = GetList()->GetWindow(); if (!w) return; CalendarView *cv = NULL; if (!w->GetViewById(IDC_CALENDAR, cv)) return; if (IsDelete) cv->OnSourceDelete(this); else cv->OnContentsChanged(this); } bool FolderCalendarSource::Read() { if (!Folder) { if (Id) { LString k = GetKey(); LXmlTag *t = App->GetOptions()->LockTag(k, _FL); if (t) { char *Col = t->GetAttr("Colour"); if (Col) Colour.Set((uint32_t)atoi64(Col), 32); else Colour.Empty(); Path = t->GetAttr("Path"); Display = t->GetAsInt("Display"); Folder = App->GetFolder(Path); App->GetOptions()->Unlock(); OnChange(false); return true; } } else LAssert(0); } return Folder != NULL; } bool FolderCalendarSource::Write() { LVariant v; if (!Id) { LXmlTag *t = App->GetOptions()->LockTag(OPT_CalendarSources, _FL); if (t) { LString Key; for (int i=0; i<100; i++) { Key.Printf("Source-%i", LRand(10000)); if (!t->GetChildTag(Key)) { Id = Key; break; } } App->GetOptions()->Unlock(); } } if (Id) { LString Key = GetKey(); LXmlTag *t = App->GetOptions()->LockTag(Key, _FL); if (!t) { App->GetOptions()->CreateTag(Key); t = App->GetOptions()->LockTag(Key, _FL); } if (t) { SaveAttr(t, CalendarSource::OptPath, Path); t->SetAttr(CalendarSource::OptColour, (int64) Colour.c32()); t->SetAttr(CalendarSource::OptDisplay, Display); t->SetAttr(CalendarSource::OptObject, GetClass()); App->GetOptions()->Unlock(); } else return false; } return true; } Calendar *FolderCalendarSource::NewEvent() { Calendar *c = new Calendar(App); if (!c) { return NULL; } c->App = App; if (!Folder) { Folder = App->GetFolder(Path); } if (!Folder) { LAssert(!"No folder?"); DeleteObj(c); return NULL; } LDataStoreI *Ms = Folder->GetObject()->GetStore(); if (!Ms) { LAssert(!"No mail store?"); DeleteObj(c); return NULL; } c->SetObject(Ms->Create(c->Type()), false, _FL); SetParentFolder(c, Folder); return c; } bool FolderCalendarSource::Match(char *Email) { bool Status = false; return Status; } void FolderCalendarSource::EditPath(LView *parent, CalendarView *cv) { if (!GetPath()) return; auto Dlg = new FolderDlg(parent, App, MAGIC_CALENDAR); Dlg->DoModal([this, Dlg, cv](auto dlg, auto ctrlId) { if (ctrlId) { SetPath(Dlg->Get()); if (cv) cv->OnContentsChanged(this); } delete dlg; }); } -bool FolderCalendarSource::GetEvents(LDateTime &StartTs, LDateTime &EndTs, LArray &Events) +bool FolderCalendarSource::GetEvents(const LDateTime StartTs, + const LDateTime EndTs, + std::function&)> Callback) { Read(); - if (!Display) - return false; - - LDateTime Start = StartTs; - Start.ToUtc(); - LDateTime End = EndTs; - End.ToUtc(); - - LArray Search; - if (!Folder) + if (!Callback) return false; - Folder->LoadThings(NULL, [&](auto Status) + if (!Display || !Folder) + { + LArray Empty; + Callback(Empty); + return false; + } + + Folder->LoadThings(NULL, [this, StartTs, EndTs, Callback](auto Status) { + LArray Events; + + LDateTime Start = StartTs; + Start.ToUtc(); + LDateTime End = EndTs; + End.ToUtc(); + + LArray Search; + for (auto t : Folder->Items) { Calendar *c = t->IsCalendar(); if (c) Search.Add(c); } for (auto c: Search) { LDateTime s, e; if (c->GetCalType() == CalEvent && c->GetField(FIELD_CAL_START_UTC, s)) { int Recur = 0; c->GetField(FIELD_CAL_RECUR, Recur); const char *Sub = NULL; c->GetField(FIELD_CAL_SUBJECT, Sub); if (Recur) { LArray Times; if (c->GetTimes(Start, End, Times)) { SetCalendarsSource(c); for (auto &t: Times) { t.src = this; Events.Add(t); } } } else { if (!c->GetField(FIELD_CAL_END_UTC, e)) { e = s; e.AddHours(1); } #if 0 printf("%s: %s > %s, %s < %s\n", Sub, s.Get().Get(), End.Get().Get(), e.Get().Get(), Start.Get().Get()); #endif if (s > End || e < Start) { // Is before/after the range } else { TimePeriod &tp = Events.New(); tp.src = this; tp.c = c; tp.s = s; tp.e = e; tp.ToLocal(); SetCalendarsSource(c); } } } } + + Callback(Events); }); return true; } void FolderCalendarSource::OnMouseClick(LMouse &m) { if (m.IsContextMenu()) { } else if (m.Down() && m.Left() && Parent) { int Col = Parent->ColumnAtX(m.x); if (Col == 0) { Display = !Display; Update(); Parent->SendNotify(LNotifyValueChanged); } else if (Col > 0) { SetCreateIn(this); } } } void FolderCalendarSource::OnPaintColumn(LItem::ItemPaintCtx &Ctx, int i, LItemColumn *c) { if (i == 0) { LRect r = Ctx; Ctx.pDC->Colour(Ctx.Back); for (int i=0; i<4; i++) { Ctx.pDC->Box(&r); r.Inset(1, 1); } Ctx.pDC->Colour(Colour); if (Display) Ctx.pDC->Rectangle(&r); else { Ctx.pDC->Box(&r); r.Inset(1, 1); Ctx.pDC->Colour(Ctx.Back); Ctx.pDC->Rectangle(&r); } } else { bool PathErr = (i == 1 && Path && !Folder); if (PathErr) Ctx.Fore = LColour::Red; LListItem::OnPaintColumn(Ctx, i, c); if (PathErr) { Ctx.pDC->Colour(Ctx.Fore); int Cy = Ctx.y1 + (Ctx.Y() >> 1) + 1; Ctx.pDC->Line(Ctx.x1, Cy, Ctx.x2, Cy); } } } const char *FolderCalendarSource::GetText(int i) { if (i == 1) { if (Folder && !Path) Path = Folder->GetPath(); return Path; } return NULL; } CalendarSource *CalendarSource::CreateIn = 0; void CalendarSource::SetCreateIn(CalendarSource *New) { if (CreateIn != New) { CreateIn = New; if (CreateIn) { if (CreateIn->Id) { LVariant v; v = CreateIn->Id.Get(); CreateIn->App->GetOptions()->SetValue(OPT_CalendarCreateIn, v); } else if (CreateIn->App) { CreateIn->App->GetOptions()->DeleteValue(OPT_CalendarCreateIn); } } } } diff --git a/Code/ImpExp_Outlook.cpp b/Code/ImpExp_Outlook.cpp --- a/Code/ImpExp_Outlook.cpp +++ b/Code/ImpExp_Outlook.cpp @@ -1,3935 +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()) { 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()) { 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... auto t = LString(GetCtrlName(IDC_FOLDER)).SplitDelimit("/"); if (t.Length() > 0) { LComPtr 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; + LPassword 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_CsvContacts.cpp b/Code/Imp_CsvContacts.cpp --- a/Code/Imp_CsvContacts.cpp +++ b/Code/Imp_CsvContacts.cpp @@ -1,581 +1,581 @@ #include "Scribe.h" #include "resdefs.h" #include "lgi/common/Db.h" #include "lgi/common/XmlTree.h" #include "lgi/common/LgiRes.h" #include "lgi/common/FileSelect.h" ////////////////////////////////////////////////////////////////////////// class LFieldMap : public LListItem { LDbField &From; ItemFieldDef *To; LString Txt; public: LFieldMap(LDbField &from) : From(from) { To = 0; #ifdef WIN32 Txt = LFromNativeCp(From.Name()); #else Txt = From.Name(); #endif for (ItemFieldDef *f = ContactFieldDefs; f->Option; f++) { if (_stricmp(From.Name(), f->DisplayText) == 0) { To = f; break; } } } int FieldId() { return To ? To->FieldId : -1; } void FieldId(int i) { To = GetFieldDefById(i); Update(); } const char *GetText(int c) { switch (c) { case 0: { return Txt; } case 1: { if (To) { const char *n = LLoadString(To->FieldId); return n ? n : To->DisplayText; } break; } } return 0; } void OnMouseClick(LMouse &m) { auto RClick = new LSubMenu; if (RClick) { int n=0; for (ItemFieldDef *f = ContactFieldDefs; f->Option; f++) { const char *Name = LLoadString(f->FieldId); RClick->AppendItem(Name ? Name : f->DisplayText, 1000+n++, true); } if (Parent->GetMouse(m, true)) { int i = RClick->Float(Parent, m.x, m.y, m.Right()); if (i>=1000) { To = ContactFieldDefs + i - 1000; Update(); } } DeleteObj(RClick); } } void Convert(Contact *c) { if (From && To) { LVariant v; if (From.Get(v)) { char *s = v.Str(); if (s) { if (To->FieldId != FIELD_NOTE) { char *Out = s; for (char *In=s; *In; In++) { if (*In != '\r' && *In != '\n') { *Out++ = *In; } } *Out++ = 0; } c->Set(To->Option, s); } } } } }; class LImpCsv : public LDialog { ScribeWnd *App = NULL; LList *Map = NULL; public: LAutoPtr Database; List Mapping; ScribeFolder *Folder = NULL; bool Merge = false; LImpCsv(ScribeWnd *app, LDb *db) { Database.Reset(db); SetParent(App = app); if (LoadFromResource(IDD_CSV_IMPORT)) { MoveToCenter(); if (GetViewById(IDC_MAPPING, Map)) Map->DrawGridLines(true); } Folder = App->GetCurrentFolder(); if (Folder && Folder->GetItemType() != MAGIC_CONTACT) Folder = App->GetFolder(FOLDER_CONTACTS); if (Folder) SetCtrlName(IDC_FOLDER, Folder->GetPath()); } void SetRecords(LDbRecordset *Rs) { if (!Rs) return; for (int i=0; iFields(); i++) { LDbField &Fld = (*Rs)[i]; auto m = new LFieldMap(Fld); if (m) { Mapping.Insert(m); Map->Insert(m); } } } void SaveMapping(const char *File) { LFile f; if (Map && f.Open(File, O_WRITE)) { f.SetSize(0); LXmlTree Xml; LXmlTag Root; Root.SetTag("field-map"); List All; Map->GetAll(All); for (auto i: All) { LXmlTag *c; Root.InsertTag(c = new LXmlTag); if (c) { c->SetTag("mapping"); c->SetAttr("from", i->GetText(0)); if (i->FieldId() >= 0) { char s[32]; sprintf_s(s, sizeof(s), "%i", i->FieldId()); c->SetAttr("to", s); } } } Xml.Write(&Root, &f); } else { LgiMsg(this, "Couldn't open '%s'\n", AppName, MB_OK, File); } } void LoadMapping(const char *File) { LFile f; if (Map && f.Open(File, O_READ)) { LXmlTree Xml; LXmlTag Root; Root.SetTag("field-map"); if (Xml.Read(&Root, &f, 0)) { List All; Map->GetAll(All); for (auto i: All) { for (auto t: Root.Children) { char *From1 = t->GetAttr("from"); const char *From2 = i->GetText(0); if (From1 && From2 && _stricmp(From1, From2) == 0) { char *Id; if ((Id = t->GetAttr("to"))) { i->FieldId(atoi(Id)); } break; } } } } } else { LgiMsg(this, "Couldn't open '%s'\n", AppName, MB_OK, File); } } int OnNotify(LViewI *c, LNotification n) { switch (c->GetId()) { case IDC_SAVE: { auto s = new LFileSelect(this); s->Type("XML", "*.xml"); s->Type("All Files", LGI_ALL_FILES); - s->Save([&](auto dlg, auto id) + s->Save([this](auto s, auto ok) { - if (id) + if (ok) SaveMapping(s->Name()); - delete dlg; + delete s; }); break; } case IDC_LOAD: { auto s = new LFileSelect(this); s->Type("XML", "*.xml"); s->Type("All Files", LGI_ALL_FILES); s->Open([this](auto dlg, auto id) { if (id) LoadMapping(dlg->Name()); delete dlg; }); break; } case IDC_BROWSE_FOLDER: { auto Dlg = new FolderDlg(this, App, MAGIC_CONTACT); Dlg->DoModal([this, Dlg](auto dlg, auto id) { if (id) { auto NewPath = Dlg->Get(); if (NewPath) { this->Folder = App->GetFolder(NewPath); if (this->Folder) SetCtrlName(IDC_FOLDER, this->Folder->GetPath()); } } delete dlg; }); break; } case IDOK: { Merge = GetCtrlValue(IDC_MODE) == 0; } case IDCANCEL: { EndModal(c->GetId() == IDOK); break; } } return 0; } }; bool ImgCsvMatch(const char *a, const char *b) { if (!a && !b) { return true; } if (a && b) { return _stricmp(a, b) == 0; } return false; } void ImportCsv(ScribeWnd *App) { auto s = new LFileSelect(App); s->Type("Comma Separated Text", "*.csv;*.txt"); s->Type("All Files", LGI_ALL_FILES); s->Open([App](auto s, auto status) { LAutoPtr mem(s); if (!status || !LFileExists(s->Name())) return; auto Db = OpenCsvDatabase(s->Name()); auto *Dlg = new LImpCsv(App, Db); if (!Dlg) return; Dlg->SetRecords(Dlg->Database->TableAt(0)); Dlg->DoModal([Dlg, App](auto dlg, auto id) { LAutoPtr mem(dlg); if (!id) return; if (!Dlg->Folder) return; LDbRecordset *Rs = Dlg->Database->TableAt(0); for (bool b=Rs->MoveFirst(); b; b=Rs->MoveNext()) { Contact *c = new Contact(App); if (c) { c->App = App; for (auto fm: Dlg->Mapping) fm->Convert(c); if (Dlg->Merge) { Contact *m = 0; const char *CFirst = 0, *CLast = 0; c->Get(OPT_First, CFirst); c->Get(OPT_Last, CLast); for (auto t: Dlg->Folder->Items) { Contact *i = t->IsContact(); if (i) { const char *IFirst = 0, *ILast = 0; i->Get(OPT_First, IFirst); i->Get(OPT_Last, ILast); if (ImgCsvMatch(IFirst, CFirst) && ImgCsvMatch(ILast, CLast)) { m = i; break; } } } if (m) { // Convert across fields. for (ItemFieldDef *Def = ContactFieldDefs; Def->FieldId; Def++) { const char *s; if (c->Get(Def->DisplayText, s)) { #ifdef WIN32 auto t = LFromNativeCp(s); if (t) m->Set(Def->Option, t); #else m->Set(Def->Option, s); #endif } } m->Save(); c->DecRef(); c = NULL; m->Update(); } else { // No match, new entry c->Save(Dlg->Folder); } } else { c->Save(Dlg->Folder); } } } }); }); } class LExportCsv : public LDialog { ScribeWnd *App; public: char *Folder; bool SubFolders; LExportCsv(ScribeWnd *app) { Folder = 0; SubFolders = false; SetParent(App = app); if (LoadFromResource(IDD_CSV_EXPORT)) { MoveToCenter(); SetCtrlEnabled(IDC_FOLDERS, false); LString p; ScribeFolder *f = App->GetCurrentFolder(); if (f && f->GetItemType() == MAGIC_CONTACT && (p = f->GetPath()) != 0) { SetCtrlName(IDC_FOLDERS, p); } else if ((f = App->GetFolder(FOLDER_CONTACTS)) != 0 && (p = f->GetPath()) != 0) { SetCtrlName(IDC_FOLDERS, p); } } } ~LExportCsv() { DeleteArray(Folder); } int OnNotify(LViewI *v, LNotification n) { switch (v->GetId()) { case IDC_SET_FOLDER: { auto Dlg = new FolderDlg(this, App, MAGIC_CONTACT); Dlg->DoModal([this, Dlg](auto dlg, auto id) { if (id) SetCtrlName(IDC_FOLDERS, Dlg->Get()); delete dlg; }); break; } case IDOK: { Folder = NewStr(GetCtrlName(IDC_FOLDERS)); SubFolders = GetCtrlValue(IDC_SUB_FOLDERS) != 0; } case IDCANCEL: { EndModal(v->GetId() == IDOK); break; } } return 0; } }; void ExportCsv(ScribeWnd *App) { auto Dlg = new LExportCsv(App); Dlg->DoModal([Dlg, App](auto dlg, auto id) { if (id) { ScribeFolder *Folder = App->GetFolder(Dlg->Folder); if (Folder) { auto s = new LFileSelect(App); s->Type("Comma Separated Text", "*.csv"); s->Type("All Files", LGI_ALL_FILES); s->Save([App, SubFolders = Dlg->SubFolders, Folder](auto s, auto status) { if (status) { LString MsgStr = AskOverwriteMsg(s->Name()); bool Exists = LFileExists(s->Name()); if (!Exists || LgiMsg(App, MsgStr, AppName, MB_YESNO) == IDYES) { int Exported = 0; const char *Error = 0; if (Exists) FileDev->Delete(s->Name()); auto Db = OpenCsvDatabase(s->Name()); LAssert(Db != NULL); if (Db) { LDbRecordset *Rs = Db->TableAt(0); LAssert(Rs != NULL); if (Rs) { for (ItemFieldDef *f=ContactFieldDefs; f->Option && f->FieldId; f++) { Rs->InsertField(f->DisplayText, GV_STRING); } Rs->InsertField("AltEmail", GV_STRING); List Contacts; App->GetContacts(Contacts, Folder, SubFolders); if (Contacts[0]) { for (auto c: Contacts) { if (Rs->AddNew()) { int Index = 0; for (ItemFieldDef *f=ContactFieldDefs; f->Option && f->FieldId; f++, Index++) { const char *n; if (c->Get(f->Option, n)) { LVariant v(n); (*Rs)[Index].Set(v); } } auto Emails = LString(",").Join(c->GetEmails().Slice(1)); if (Emails) (*Rs)[Index] = Emails; if (Rs->Update()) Exported++; } } } else Error = "No Contacts."; } else Error = "Couldn't open record set."; DeleteObj(Db); } else Error = "Couldn't open database."; LgiMsg(App, LLoadString(IDS_EXPORT_MSG), AppName, MB_OK, Exported, Error?Error:(char*)""); } } delete s; }); } } delete dlg; }); } diff --git a/Code/Imp_OutlookExpress.cpp b/Code/Imp_OutlookExpress.cpp --- a/Code/Imp_OutlookExpress.cpp +++ b/Code/Imp_OutlookExpress.cpp @@ -1,719 +1,719 @@ /* ** FILE: Imp_OutlookExpress.cpp ** AUTHOR: Matthew Allen ** DATE: 4/2/2000 ** DESCRIPTION: Scribe importer ** ** Copyright (C) 2000, Matthew Allen ** fret@memecode.com */ #include "Scribe.h" #include "resdefs.h" #include "lgi/common/ProgressDlg.h" #include "lgi/common/LgiRes.h" #include "lgi/common/FileSelect.h" bool ImportMBX(ScribeWnd *Parent, ScribeFolder *ParentFolder, char *FileName) { LFile F; if (FileName && ParentFolder && Parent && F.Open(FileName, O_READ)) { char Magic[4] = {'J', 'M', 'F', '6'}; char Buf[4]; if (F.Read(Buf, 4) == 4 && memcmp(Magic, Buf, 4) == 0) { int32 Msgs; F.Seek(4, SEEK_CUR); F >> Msgs; // Create folder for these messages char n[256]; strcpy_s(n, sizeof(n), FileName); char *Ls = 0; // last slash for (Ls = n+strlen(n)-1; Ls > n; Ls--) { if (*Ls == DIR_CHAR) { Ls++; break; } } char *Dot = strchr(Ls, '.'); if (Dot) { *Dot = 0; } ScribeFolder *Folder = ParentFolder->CreateSubDirectory(Ls, MAGIC_MAIL); if (Folder) { // Skip unknown feilds F.Seek(4 + 4 + 1 + 63, SEEK_CUR); // Read the messages for (int i=0; i> MsgId; int MsgNum = 0; F >> MsgNum; int MsgTotalSize = 0; F >> MsgTotalSize; int MsgTextSize; F >> MsgTextSize; // Create item Mail *m = dynamic_cast(Parent->CreateItem(MAGIC_MAIL, Folder, false)); if (m) { LMemStream Text(&F, 0, MsgTextSize); // Decode email into fields m->OnAfterReceive(&Text); m->SetFlags(MAIL_RECEIVED | MAIL_READ | ((m->HasAttachments()) ? MAIL_ATTACHMENTS : 0) ); } // Seek to the next message F.Seek(MsgStartPos+MsgTotalSize, SEEK_SET); } } else { return false; } return true; } } return false; } #ifdef WIN32 #pragma pack(push, 1) #endif class DbxMessagePtr { public: uint MessagePos; uint TablePos; char Reserved[4]; }; // #define DbxTableSize 0x27c class DbxTable { public: // Header /* uint FilePos; char Reserved1[13]; uint Msgs; char Reserved2[3]; */ uint FilePos; char Reserved1[4]; uint ListPtr; uint NextPtr; char Reserved2[8]; }; class DbxMsgTable { public: uint FilePos; uint MessageLength; char Reserved[17]; uint FirstBlockPos; }; class DbxMsgBlock { public: /* uint FilePos; uint Flags; uint BlockSize; uint NextBlockPos; */ uint FilePos; uint Increase; uint Include; uint Next; uint UseNet; }; #ifdef WIN32 #pragma pack(pop) #endif class ImportDBX { private: ScribeWnd *Parent = NULL; ScribeFolder *ParentFolder = NULL; LFile F; uint64 FileSize = 0; List Used; class MsgInfo { public: int Pos; bool IsNews; MsgInfo(int p, bool n) { Pos = p; IsNews = n; } }; List MsgList; bool IsUsed(ssize_t i) { for (auto n: Used) { if (*n == i) return true; } return false; } void SetUsed(ssize_t i) { if (!IsUsed(i)) { Used.Insert(new ssize_t(i)); } } bool ReadMessage(ssize_t Pos, bool IsNews, ScribeFolder *Folder) { bool Status = false; if (!IsUsed(Pos) && F.Seek(Pos, SEEK_SET)) { LTempStream Text(ScribeTempPath()); SetUsed(Pos); while (Pos > 0) { DbxMsgBlock Msg; ZeroObj(Msg); F.Read(&Msg, sizeof(Msg)-4); if (Pos != Msg.FilePos) { break; } Pos += sizeof(Msg)-4; // int Next = Pos + Msg.Next; auto End = Pos + Msg.Include; char Buf[1024]; while (Pos < End) { ssize_t R = F.Read(Buf, MIN(End-Pos, sizeof(Buf))); Text.Write(Buf, R); Pos += R; } F.Seek(Msg.Next, SEEK_SET); Pos = Msg.Next; } Mail *m = new Mail(Parent); if (m) { m->App = Parent; m->OnAfterReceive(&Text); m->SetFlags(MAIL_RECEIVED | MAIL_READ | // ((MsgTable.Reserved[3] == 3) ? MAIL_READ : 0) | (m->GetAttachments(0) ? MAIL_ATTACHMENTS : 0)); m->SetDirty(); m->Save(Folder); Status = true; } } return Status; } bool ReadMsgHeader(int Pos) { bool Status = false; if (!IsUsed(Pos) && F.Seek(Pos, SEEK_SET)) { SetUsed(Pos); DbxMsgBlock Msg; F.Read(&Msg, sizeof(Msg)); if (Msg.FilePos == Pos) { Pos += sizeof(Msg); int32 Self, MsgPos = 0, NewsPost = false, Count = 0; do { F.Read(&Self, sizeof(Self)); if ((Self & 0xff) == 0x84) { if (!MsgPos) { MsgPos = Self >> 8; } } if ((Self & 0xff) == 0x83) { NewsPost = true; } Count++; } while ((Self & 0x7f) > 0); if (MsgPos) { MsgList.Insert(new MsgInfo(MsgPos, NewsPost != 0)); Status |= true; } else { F.Read(&Self, sizeof(Self)); F.Read(&MsgPos, sizeof(MsgPos)); MsgList.Insert(new MsgInfo(MsgPos, NewsPost != 0)); Status |= true; } } } return Status; } bool ReadTable(ssize_t Pos) { bool Status = false; if (Pos > 0 && (uint64)Pos < FileSize && !IsUsed(Pos)) { DbxTable Tbl; F.Seek(Pos, SEEK_SET); SetUsed(Pos); F.Read(&Tbl, sizeof(Tbl)); if (Tbl.FilePos == Pos) { Pos += sizeof(Tbl); Status |= ReadTable(Tbl.NextPtr); Status |= ReadTable(Tbl.ListPtr); F.Seek(Pos, SEEK_SET); while (true) { bool Actioned = false; DbxMessagePtr Node; F.Seek(Pos, SEEK_SET); Pos += F.Read(&Node, sizeof(Node)); if (Node.MessagePos > 0 && Node.MessagePos < FileSize && Node.MessagePos != Node.TablePos) { Actioned |= ReadMsgHeader(Node.MessagePos); } if (Node.TablePos > 0 && Node.TablePos < FileSize && Node.TablePos != Node.MessagePos) { Actioned |= ReadTable(Node.TablePos); } if (Actioned) { Status = true; } else break; } } } return Status; } void Clean() { Used.DeleteObjects(); MsgList.DeleteObjects(); } public: ImportDBX(ScribeWnd *parent) { Parent = parent; ParentFolder = 0; } ~ImportDBX() { Clean(); } bool Import(ScribeFolder *parentFolder, char *FileName) { Clean(); ParentFolder = parentFolder; if (sizeof(DbxTable) != 24) { // packing is screwed LgiMsg(Parent, "Compiled object 'DbxTable' is the wrong size", AppName, MB_OK); return false; } LProgressDlg Dlg(Parent); Dlg.SetDescription("Reading tables..."); Dlg.SetRange(1); LProgressPane *Import = Dlg.Push(); if (Import) Import->SetDescription("Importing messages..."); if (Parent && ParentFolder && FileName && F.Open(FileName, O_READ)) { FileSize = F.GetSize(); // Create folder for these messages char n[256]; strcpy_s(n, sizeof(n), FileName); char *Ls = strrchr(n, DIR_CHAR); // last slash if (Ls) { Ls++; char *Dot = strchr(Ls, '.'); if (Dot) { *Dot = 0; } // Make sure the path is unique, no overwriting previous folders LStringPipe NewPath(256); auto ParentPath = ParentFolder->GetPath(); NewPath.Print("/%s/%s", ParentPath.Get(), Ls); auto BaseNewPath = NewPath.NewLStr(); LString NewPathStr = BaseNewPath.Get(); while (Parent->GetFolder(NewPathStr)) { char *Num = NewPathStr.Get() + BaseNewPath.Length() - 1; for (; Num>NewPathStr && IsDigit(*Num); Num--) ; if (!IsDigit(*Num)) Num++; int i = atoi(Num); NewPath.Print("%s%i", BaseNewPath.Get(), i+1); NewPathStr = NewPath.NewLStr(); } // Create output folder ScribeFolder *Folder = ParentFolder->CreateSubDirectory(Ls, MAGIC_MAIL); if (Folder) { // Read header F.Seek(0x30, SEEK_SET); uint TableLoc; F >> TableLoc; if (!TableLoc) TableLoc = 0x1e254; ReadTable(TableLoc); // Import msg list Dlg.Value(1); if (Import) { Import->SetRange(MsgList.Length()); Import->SetType("Mail"); } for (auto i: MsgList) { ReadMessage(i->Pos, i->IsNews, Folder); if (Import) Import->Value(Import->Value()+1); } /* List Msgs; while (Tbl) { int Pos = F.GetPosition(); F.Read(Tbl, DbxTableSize); if (Tbl->FilePos == Pos) { for (int i=0; iMsgs; i++) { Msgs.Insert(new int(Tbl->Msg[i].MessagePos)); } } if (Tbl->Msgs < 76 || Tbl->FilePos != Pos) { DeleteArray(Tbl); } } */ // Read in messages /* for (int *MsgPos = Msgs.First(); MsgPos; MsgPos = Msgs.Next()) { bool Read = true; DbxMsgTable MsgTable; F.Seek(*MsgPos, SEEK_SET); F.Read(&MsgTable, sizeof(MsgTable)); if (MsgTable.FilePos == *MsgPos) { // Read in blocks GBytePipe Msg; DbxMsgBlock MsgBlock; MsgBlock.NextBlockPos = MsgTable.FirstBlockPos; while (MsgBlock.NextBlockPos) { int SeekTo = MsgBlock.NextBlockPos & 0xFFFFFF; F.Seek(SeekTo, SEEK_SET); F.Read(&MsgBlock, sizeof(MsgBlock)); if (MsgBlock.FilePos == SeekTo) { uchar *Data = new uchar[MsgBlock.BlockSize]; if (Data) { F.Read(Data, MsgBlock.BlockSize); Msg.Push(Data, MsgBlock.BlockSize); DeleteArray(Data); } } } // Msg contains email... int Size = Msg.Sizeof(); if (Size > 0) { Mail *m = dynamic_cast(Parent->CreateItem(MAGIC_MAIL, Folder, false)); if (m) { m->Text = new char[Size+1]; if (m->Text) { // Read data into item Msg.Pop((uchar*) m->Text, Size); m->Text[Size] = 0; // Decode email into fields m->OnAfterReceive(); m->SetFlags(MAIL_RECEIVED | ((MsgTable.Reserved[3] == 3) ? MAIL_READ : 0) | ((m->Store->GetChild() != 0) ? MAIL_ATTACHMENTS : 0)); m->StoreDirty = true; } } } } } */ return true; } } } return false; } }; struct ImportOe : public LProgressDlg { ScribeWnd *App; bool v5; int Imported = 0, TotalFiles = 0; LString Dir; LArray Files; LArray Ext; LString::Array FileArr; ScribeFolder *Folder = NULL; ImportOe(ScribeWnd *app, bool ver5) : App(app), v5(ver5), LProgressDlg(app) { // Get the base directory - auto Dir = LGetSystemPath(LSP_LOCAL_APP_DATA); + Dir = LGetSystemPath(LSP_LOCAL_APP_DATA); if (!Dir) // Just in case Dir = LGetSystemPath(LSP_OS); // Search for the folder files Ext.Add(v5 ? "*.dbx" : "*.mbx"); DoFileSearch(); if (Files.Length() == 0) { if (LgiMsg( App, "%i outlook express data files found in:\n" "%s\n" "Do you want to select a different directory to search?", AppName, MB_YESNO, (int)Files.Length(), Dir.Get()) == IDYES) { auto Select = new LFileSelect(App); - Select->OpenFolder([&](auto dlg, auto status) + Select->OpenFolder([this](auto s, auto ok) { - if (status) + if (ok) { - strcpy_s(Dir, sizeof(Dir), Select->Name()); + Dir = s->Name(); DoFileSearch(); ProcessFiles(); } - delete dlg; + delete s; }); } } else ProcessFiles(); } void DoFileSearch() { LRecursiveFileSearch(Dir, &Ext, &Files); } void ProcessFiles() { if (Files.Length() == 0) return; // Strip files for (unsigned i=0; iGetCurrentFolder(); LString CurrentPath; if (Current) CurrentPath = Current->GetPath(); auto Dlg = new ChooseFolderDlg( App, false, AppName, LLoadString(IDS_OE_IMPORT), CurrentPath, MAGIC_MAIL, &FileArr); Dlg->DoModal([this, Dlg](auto dlg, auto id) { LAutoPtr mem(dlg); if (!id) return; ScribeFolder *Folder = App->GetFolder(Dlg->DestFolder); if (!Folder) { LgiMsg(App, "Error locating that folder.", AppName, MB_OK); return; } SetDescription("Importing folders..."); SetRange(FileArr.Length()); FileArr = Dlg->SrcFiles; TotalFiles = (int)FileArr.Length(); SetPulse(100); // Start processing... each timeout allows the message loop to run a bit }); }; void OnFinished() { LgiMsg( App, "%i of %i %s files imported successfully.", AppName, MB_OK, Imported, TotalFiles, (v5) ? (char*)"DBX" : (char*)"MBX"); delete this; } void OnPulse() { if (FileArr.Length()) { auto FileName = FileArr.Last(); FileArr.PopLast(); if (v5) { ImportDBX Filter(App); if (Filter.Import(Folder, FileName)) Imported++; } else { if (ImportMBX(App, Folder, FileName)) Imported++; } (*this)++; // inc progress bar... } else OnFinished(); } }; void Import_OutlookExpress(ScribeWnd *Parent, bool v5) { new ImportOe(Parent, v5); } diff --git a/Code/ManageMailStores.cpp b/Code/ManageMailStores.cpp --- a/Code/ManageMailStores.cpp +++ b/Code/ManageMailStores.cpp @@ -1,607 +1,607 @@ #include "lgi/common/Lgi.h" #include "Scribe.h" #include "Store3Mail3/Mail3.h" #include "ManageMailStores.h" #include "lgi/common/List.h" #include "lgi/common/ListItemCheckBox.h" #include "lgi/common/Edit.h" #include "lgi/common/LgiRes.h" #include "lgi/common/FileSelect.h" #include "resdefs.h" class FmtDlg : public LDialog { public: int Ver; FmtDlg(LViewI *p, int ver) { Ver = -1; SetParent(p); if (LoadFromResource(IDD_FOLDER_FORMAT)) { MoveToCenter(); SetCtrlValue(IDC_FORMAT, ver); } } int OnNotify(LViewI *Ctrl, LNotification n) { switch (Ctrl->GetId()) { case IDOK: { Ver = (int)GetCtrlValue(IDC_FORMAT); // Fall through } case IDCANCEL: { EndModal(Ctrl->GetId() == IDOK); break; } } return 0; } }; //////////////////////////////////////////////////////////////////////////////////////// class SubFolderDlg : public LDialog, public LXmlTreeUi { ScribeWnd *App; void FolderSelector(int OutputCtrl, int Limit) { auto Dlg = new FolderDlg(this, App, Limit); Dlg->DoModal([this, Dlg, OutputCtrl](auto dlg, auto id) { if (id) { LEdit *e; if (GetViewById(OutputCtrl, e)) e->Name(Dlg->Get()); } delete dlg; }); } public: SubFolderDlg(LView *parent, ScribeWnd *app) { App = app; SetParent(parent); if (App->GetOptions() && LoadFromResource(IDD_SUB_FOLDERS)) { Map(OPT_Inbox, IDC_INBOX, GV_STRING); Map(OPT_Outbox, IDC_OUTBOX, GV_STRING); Map(OPT_Sent, IDC_SENT, GV_STRING); Map(OPT_Trash, IDC_TRASH, GV_STRING); Map(OPT_Contacts, IDC_CONTACT_FLD, GV_STRING); Map(OPT_Templates, IDC_TEMPLATES, GV_STRING); Map(OPT_Calendar, IDC_CALENDER, GV_STRING); Map(OPT_Filters, IDC_FILTERS_FLD, GV_STRING); Map(OPT_Groups, IDC_GROUPS_FLD, GV_STRING); Map(OPT_SpamFolder, IDC_SPAM_FLD, GV_STRING); Map(OPT_HasTemplates, IDC_HAS_TEMPLATES, GV_BOOL); Map(OPT_HasGroups, IDC_HAS_GROUPS, GV_BOOL); Map(OPT_HasCalendar, IDC_HAS_CAL_EVENTS, GV_BOOL); Map(OPT_HasFilters, IDC_HAS_FILTERS, GV_BOOL); Map(OPT_HasSpam, IDC_HAS_SPAM, GV_BOOL); Convert(App->GetOptions(), this, true); MoveToCenter(); } } int OnNotify(LViewI *Ctrl, LNotification n) { switch (Ctrl->GetId()) { case IDOK: Convert(App->GetOptions(), this, false); EndModal(1); break; case IDCANCEL: EndModal(0); break; case IDC_SET_INBOX: FolderSelector(IDC_INBOX, MAGIC_MAIL); break; case IDC_SET_OUTBOX: FolderSelector(IDC_OUTBOX, MAGIC_MAIL); break; case IDC_SET_SENT: FolderSelector(IDC_SENT, MAGIC_MAIL); break; case IDC_SET_TRASH: FolderSelector(IDC_TRASH, MAGIC_ANY); break; case IDC_SET_CONTACTS: FolderSelector(IDC_CONTACT_FLD, MAGIC_CONTACT); break; case IDC_SET_TEMPLATES: FolderSelector(IDC_TEMPLATES, MAGIC_MAIL); break; case IDC_SET_CALENDER: FolderSelector(IDC_CALENDER, MAGIC_CALENDAR); break; case IDC_SET_FILTERS: FolderSelector(IDC_FILTERS_FLD, MAGIC_FILTER); break; case IDC_SET_GROUPS: FolderSelector(IDC_GROUPS_FLD, MAGIC_GROUP); break; case IDC_SET_SPAM: FolderSelector(IDC_SPAM_FLD, MAGIC_MAIL); break; } return 0; } }; //////////////////////////////////////////////////////////////////////////////////////// LString FolderFullPath(const char *Path) { char f[MAX_PATH_LEN]; if (LIsRelativePath(Path)) { LMakePath(f, sizeof(f), LGetExePath(), Path); Path = f; } auto Ext = LGetExtension(Path); if (!Stricmp(Ext, "sqlite")) { LString s = Path; LTrimDir(s); return s; } return Path; } int GetFolderVersion(const char *f) { auto Path = FolderFullPath(f); if (LDirExists(Path)) { char p[MAX_PATH_LEN]; LMakePath(p, sizeof(p), Path, MAIL3_DB_FILE); if (LFileExists(p)) Path = p; } if (LFileExists(Path)) { auto Ext = LGetExtension(Path); if (!Stricmp(Ext, "sqlite")) return 3; } return 0; } void EditWebdav(LViewI *parent, LXmlTag *t, std::function callback) { auto dlg = new LDialog(parent); dlg->LoadFromResource(IDD_WEBDAV_PROPS); dlg->MoveSameScreen(parent); dlg->SetCtrlName(IDC_DESC, t->GetAttr(OPT_MailStoreName)); dlg->SetCtrlName(IDC_CONTACTS_URL, t->GetAttr(OPT_MailStoreContactUrl)); dlg->SetCtrlName(IDC_CALENDAR_URL, t->GetAttr(OPT_MailStoreCalendarUrl)); dlg->SetCtrlName(IDC_USERNAME, t->GetAttr(OPT_MailStoreUserName)); dlg->SetCtrlName(IDC_PASSWORD, t->GetAttr(OPT_MailStorePassword)); dlg->DoModal([t, callback](auto dlg, auto res) { if (res == IDOK) { t->SetAttr(OPT_MailStoreName, dlg->GetCtrlName(IDC_DESC)); t->SetAttr(OPT_MailStoreContactUrl, dlg->GetCtrlName(IDC_CONTACTS_URL)); t->SetAttr(OPT_MailStoreCalendarUrl, dlg->GetCtrlName(IDC_CALENDAR_URL)); t->SetAttr(OPT_MailStoreUserName, dlg->GetCtrlName(IDC_USERNAME)); t->SetAttr(OPT_MailStorePassword, dlg->GetCtrlName(IDC_PASSWORD)); callback(true); } else callback(false); delete dlg; }); } class StoreItem : public LListItem { ScribeWnd *App; LListItemCheckBox *Disable; public: LXmlTag Tag; StoreItem(ScribeWnd *app) : Tag(OPT_MailStore) { App = app; Disable = new LListItemCheckBox(this, 2, false); } const char *GetText(int Col) { switch (Col) { case 0: return Tag.GetAttr(OPT_MailStoreName); case 1: return Tag.GetAttr(OPT_MailStoreLocation); } return 0; } bool SetText(const char *s, int Col) { switch (Col) { case 0: Tag.SetAttr(OPT_MailStoreName, s); break; case 1: Tag.SetAttr(OPT_MailStoreLocation, s); break; } Update(); GetList()->ResizeColumnsToContent(); return true; } bool XmlIo(LXmlTag *t, bool Write) { if (Write) { Tag.SetAttr(OPT_MailStoreDisable, (int)Disable->Value()); *t = Tag; } else { Tag = *t; int i = Tag.GetAsInt(OPT_MailStoreDisable); if (i >= 0) Disable->Value(i); } return true; } bool IsWebdav() { return Tag.GetAttr(OPT_MailStoreContactUrl) || Tag.GetAttr(OPT_MailStoreCalendarUrl); } void Edit() { - EditWebdav(GetList(), &Tag, [&](auto status) + EditWebdav(GetList(), &Tag, [this](auto status) { if (status) Update(); }); } void OnMouseClick(LMouse &m) { bool Wd = IsWebdav(); if (m.IsContextMenu()) { if (Wd) { LSubMenu s; s.AppendItem(LLoadString(IDS_EDIT), IDM_EDIT); if (s.Float(GetList(), m) == IDM_EDIT) Edit(); } } else if (m.Left() && m.Double() && m.Down()) { int Col = GetList()->ColumnAtX(m.x); switch (Col) { case 0: { EditLabel(Col); break; } case 1: { if (Wd) Edit(); else EditLabel(Col); break; } } } LListItem::OnMouseClick(m); } }; ManageMailStores::ManageMailStores(ScribeWnd *app) { Lst = 0; SetParent(App = app); if (LoadFromResource(IDD_MANAGE_FOLDERS)) { GetViewById(IDC_MAIL_STORES, Lst); MoveToCenter(); LXmlTag *Base = App->GetOptions()->LockTag(0, _FL); if (Base) { Options.Copy(*Base, false); LXmlTag *Ms = Base->GetChildTag(OPT_MailStores); if (Ms) { LXmlTag *t = Options.CreateTag(OPT_MailStores); if (t) { t->Copy(*Ms, true); } } App->GetOptions()->Unlock(); } Map(OPT_MailStores, IDC_MAIL_STORES, OPT_MailStore, [this]() { return new StoreItem(App); }); Map(OPT_StartInFolder, IDC_START_IN, GV_STRING); Convert(&Options, this, true); Lst->ResizeColumnsToContent(); OnItemSelect(); } } ManageMailStores::~ManageMailStores() { } void ManageMailStores::OnItemSelect() { LListItem *s = Lst->GetSelected(); auto Name = s ? s->GetText(1) : NULL; auto Ver = Name ? GetFolderVersion(Name) : 0; SetCtrlEnabled(IDC_COMPACT_MS, Ver > 0); SetCtrlEnabled(IDC_CONVERT_MS, Ver == 3); SetCtrlEnabled(IDC_REPAIR_MS, true); } LMailStore *ManageMailStores::GetCurrentMailStore() { LListItem *s = Lst->GetSelected(); if (s) { auto p = FolderFullPath(s->GetText(1)); for (unsigned i=0; iGetStorageFolders().Length(); i++) { LMailStore &s = App->GetStorageFolders()[i]; printf("GetCur %s %s\n", p.Get(), s.Path.Get()); if (s.Path && s.Path.Equals(p)) { if (s.Store) return &s; } } } return 0; } int ManageMailStores::OnNotify(LViewI *c, LNotification n) { if (!Lst) return 0; switch (c->GetId()) { case IDC_MAIL_STORES: { if (n.Type == LNotifyItemSelect) { OnItemSelect(); } break; } case IDC_OPEN_MS: { auto s = new LFileSelect(this); s->InitialDir(LGetExePath()); s->Type("Mail Folders", "*.mail3;*.sqlite"); s->Open([this](auto dlg, auto status) { if (status) { StoreItem *Si = new StoreItem(App); if (Si) { char b[MAX_PATH_LEN]; strcpy_s(b, sizeof(b), dlg->Name()); char *n = LGetExtension(b); if (n && !_stricmp(n, "sqlite")) { n = strrchr(b, DIR_CHAR); if (n) *n = 0; } Si->Tag.SetAttr(OPT_MailStoreLocation, b); Lst->Insert(Si); Lst->ResizeColumnsToContent(); } } delete dlg; }); break; } case IDC_CLOSE_MS: { List s; Lst->GetSelection(s); s.DeleteObjects(); Lst->ResizeColumnsToContent(); break; } case IDC_CREATE_MS: { char Opts[MAX_PATH_LEN]; LMakePath(Opts, sizeof(Opts), App->GetOptions()->GetFile(), ".."); LView *btn; if (!GetViewById(IDC_CREATE_MS, btn)) break; LSubMenu s; s.AppendItem("Mail3 Local Folders", IDM_LOCAL_FOLDERS); s.AppendItem("Webdav Remote Folder", IDM_WEBDAV_FOLDER); LPoint pt(0, btn->Y()); btn->PointToScreen(pt); int Cmd = s.Float(this, pt.x, pt.y, LSubMenu::BtnLeft); if (Cmd == IDM_LOCAL_FOLDERS) { auto s = new LFileSelect(this); s->InitialDir(Opts); s->Name((char*)"Folders.mail3"); - s->Save([&](auto dlg, auto status) + s->Save([this, Opts=LString(Opts)](auto s, auto ok) { - if (status) + if (ok) { StoreItem *Si = new StoreItem(App); if (Si) { auto Rel = LMakeRelativePath(Opts, s->Name()); Si->Tag.SetAttr(OPT_MailStoreLocation, Rel ? Rel.Get() : s->Name()); Lst->Insert(Si); Lst->ResizeColumnsToContent(); } } - delete dlg; + delete s; }); } else if (Cmd == IDM_WEBDAV_FOLDER) { StoreItem *Si = new StoreItem(App); if (!Si) break; - EditWebdav(this, &Si->Tag, [&](auto status) + EditWebdav(this, &Si->Tag, [this, Si](auto status) { if (status) { Lst->Insert(Si); Lst->ResizeColumnsToContent(); } - else DeleteObj(Si); + else delete Si; }); } break; } case IDC_COMPACT_MS: { LMailStore *ms = GetCurrentMailStore(); if (ms) { App->CompactFolders(*ms); } else { LgiMsg(this, LLoadString(IDS_ERROR_FOLDER_NOT_LOADED), AppName); } break; } case IDC_CONVERT_MS: { LMailStore *ms = GetCurrentMailStore(); if (ms) { auto Dlg = new FmtDlg(this, (int)ms->Store->GetInt(FIELD_FORMAT)); Dlg->DoModal([this, Dlg, ms](auto dlg, auto id) { if (id) { Store3Progress Prog(App, true); Prog.SetInt(Store3UiNewFormat, Dlg->Ver); if (!ms->Store->SetFormat(this, &Prog)) LgiMsg(this, "Set format failed.", AppName); } delete dlg; }); } break; } case IDC_REPAIR_MS: { LMailStore *ms = GetCurrentMailStore(); if (ms) { auto Prog = new Store3Progress(App, true); ms->Store->Repair(this, Prog, [this, Prog](auto status) { if (!status) { auto Err = Prog->GetStr(Store3UiError); LgiMsg( this, "Repair failed: %s", AppName, MB_OK, Err?Err:"Unsupported method."); } delete Prog; }); } else LgiMsg(this, "Error: No mail store selected.", AppName); break; } case IDC_SUB_FOLDERS: { auto Dlg = new SubFolderDlg(this, App); Dlg->DoModal(NULL); break; } case IDC_SET_START_IN: { auto Dlg = new FolderDlg(this, App); Dlg->DoModal([this, Dlg](auto dlg, auto id) { if (id) SetCtrlName(IDC_START_IN, Dlg->Get()); delete dlg; }); break; } case IDOK: { Convert(&Options, this, false); } case IDCANCEL: { EndModal(c->GetId() == IDOK); break; } } return 0; } diff --git a/Code/OptionsDlg.cpp b/Code/OptionsDlg.cpp --- a/Code/OptionsDlg.cpp +++ b/Code/OptionsDlg.cpp @@ -1,1282 +1,1283 @@ /* ** FILE: OptionsDlg.cpp ** AUTHOR: Matthew Allen ** DATE: 5/8/2011 ** DESCRIPTION: Scribe email options dialog ** ** Copyright (C) 2011, Matthew Allen ** fret@memecode.com */ #include #include #include #include "Scribe.h" #include "ScribePrivate.h" #include "lgi/common/Edit.h" #include "lgi/common/RadioGroup.h" #include "lgi/common/Combo.h" #include "lgi/common/Button.h" #include "lgi/common/TextView3.h" #include "lgi/common/TextLabel.h" #include "lgi/common/ControlTree.h" #include "lgi/common/TabView.h" #include "lgi/common/SpellCheck.h" #include "lgi/common/LgiRes.h" #include "lgi/common/RichTextEdit.h" #include "lgi/common/EventTargetThread.h" #include "lgi/common/TextView3.h" #include "lgi/common/FileSelect.h" #include "resdefs.h" #include "CalendarView.h" static char AutoInBrackets[] = "(auto)"; #ifndef IDC_TAB #define IDC_TAB 100 #endif #if defined WIN32 #define DLG_X 370 #define DLG_Y 420 #define TAB_X (DLG_X-26) #define TAB_Y (DLG_Y-74) #else #define DLG_X 380 #define DLG_Y 390 #define TAB_X (DLG_X-20) #define TAB_Y (DLG_Y-50) #endif static int AccountCmp(LListItem *a, LListItem *b, NativeInt Data) { return a->Compare(b); } class UtfEditor : public LWindow { ScribeWnd *App; LTabView *Tabs; LTabPage *TabText, *TabHtml; LButton *Ok, *Cancel; LTextView3 *Txt; LRichTextEdit *Html; const char *TextOpt, *HtmlOpt; public: UtfEditor(ScribeWnd *app, const char *txtopt, const char *htmlopt, const char *desc); void OnPosChange(); int OnNotify(LViewI *c, LNotification n); }; class AccountItem : public LListItem { OptionsDlg *Dlg; public: ScribeAccount *Account; LListItemCheckBox *Disable; AccountItem(OptionsDlg *d, ScribeAccount *a) { Dlg = d; Account = a; Account->Views.Add(this); Disable = 0; Disable = new LListItemCheckBox(this, 3, a->Send.Disabled() > 0); } ~AccountItem() { LAssert(Account->Views.HasItem(this)); Account->Views.Delete(this); } ScribeAccount *GetAccount() { return Account; } const char *GetText(int i); void OnMouseClick(LMouse &m) { if (m.Double() && Account) { - Dlg->App->GetAccountSettingsAccess(Dlg, ScribeReadAccess, [&](auto Allow) + Dlg->App->GetAccountSettingsAccess(Dlg, ScribeReadAccess, [this](auto Allow) { if (Allow) { - Account->InitUI(Parent, 0, [&](auto status) + Account->InitUI(Parent, 0, [this](auto status) { if (status) Update(); }); } }); } LListItem::OnMouseClick(m); } void OnColumnNotify(int Col, int64 Data) { if (Disable && Col == 3) { Dlg->OnAccountEnable(Account, !Data); } LListItem::OnColumnNotify(Col, Data); } int Compare(LListItem *To, ssize_t Field = 0) { AccountItem *ToItem = dynamic_cast(To); if (!ToItem) { printf("%s:%i - Not the right object.\n", _FL); return 0; } int a = Account->Identity.Sort(); int b = ToItem->Account->Identity.Sort(); // printf("%s:%i - %p = %i, %p = %i\n", _FL, Account, a, ToItem->Account, b); return a - b; } }; const char *AccountItem::GetText(int i) { if (Account) { switch (i) { case 0: { static char Buf[64]; LVariant Text = Account->Receive.Name(); if (!Text.Str()) { Text = Account->Receive.Server(); if (!Text.Str()) { Text = Account->Send.Server(); } } if (Text.Str()) { strcpy_s(Buf, sizeof(Buf), Text.Str()); return Buf; } break; } case 1: { return Account->Send.Server().Str() ? (char*)"yes" : 0; break; } case 2: { return Account->Receive.Server().Str() ? (char*)"yes" : 0; break; } } } return 0; } int LangCompare(LLanguage *a, LLanguage *b, NativeInt d) { return _stricmp(a->Name, b->Name); } /////////////////////////////////////////////////////////////////////////////////////////////////////// class RemoteContentDlg : public LDialog, public LXmlTreeUi { ScribeWnd *App; LOptionsFile *Opts; public: RemoteContentDlg(LView *Parent, ScribeWnd *app) : App(app) { SetParent(Parent); Opts = App->GetOptions(); if (LoadFromResource(IDD_REMOTE_CONTENT)) { Map(OPT_RemoteContentWhiteList, IDC_WHITELIST, GV_STRING); Map(OPT_RemoteContentBlackList, IDC_WHITELIST, GV_STRING); Convert(Opts, this, true); MoveSameScreen(Parent); } } int OnNotify(LViewI *Ctrl, LNotification n) { switch (Ctrl->GetId()) { case IDOK: Convert(Opts, this, false); App->RemoteContent_ClearCache(); // Fall thru case IDCANCEL: EndModal(Ctrl->GetId() == IDOK); break; } return 0; } }; /////////////////////////////////////////////////////////////////////////////////////////////////////// class OptionsDlgPrivate { public: LString::Array Langs; LString::Array Dictionaries; }; OptionsDlg::OptionsDlg(ScribeWnd *window) : TabDialog(IDC_TAB, IDC_LAUNCH_HELP) { d = new OptionsDlgPrivate; UiLang = 0; SetParent(App = window); SinkHnd = LEventSinkMap::Dispatch.AddSink(this); LRect r(0, 0, DLG_X, DLG_Y); SetPos(r); MoveSameScreen(App); if (!LoadFromResource(IDD_SETTINGS, window->GetUiTags())) { LgiMsg(window, "Options resource missing.", "Error"); return; } MoveToCenter(); LList *ACtrl; if (GetViewById(IDC_ACCOUNTS, ACtrl)) { for (auto a : *App->GetAccounts()) { AccountItem *i = new AccountItem(this, a); if (i) { ACtrl->Insert(i); if (ACtrl->Length() == 1) { i->Select(true); } } } LNotification note(LNotifyItemInsert); OnNotify(ACtrl, note); ACtrl->Sort(AccountCmp); } // Identity tab Map(OPT_UserName, IDC_NAME, GV_STRING); // Accounts tab Map(OPT_DefaultSendAccount, IDC_DEF_SEND, GV_INT32); Map(OPT_ExtraHeaders, IDC_EXTRA_HEADERS, GV_STRING); Map(OPT_HideId, IDC_HIDE_ID, GV_BOOL); // General tab Map(OPT_QuoteReply, IDC_QUOTE, GV_BOOL); Map(OPT_QuoteReplyStr, IDC_QUOTE_STR, GV_STRING); Map(OPT_ReplyWithSig, IDC_REPLYWITHSIG, GV_BOOL); Map(OPT_DefaultReplyAllSetting, IDC_DEF_REPLYALL_SETTING, GV_INT32); Map(OPT_MinimizeToTray, IDC_TRAY, GV_BOOL); Map(OPT_NewMailSoundFile, IDC_NEW_MAIL_SOUND, GV_STRING); #if WINNATIVE Map(OPT_CheckDefaultEmail, IDC_CHECK_DEFAULT, GV_BOOL); Map(OPT_RegisterWindowsClient, IDC_REGISTER_CLIENT, GV_BOOL); #endif Map(OPT_ConfirmDelete, IDC_CONFIRM_DEL, GV_BOOL); Map(OPT_DelDirection, IDC_MSG_DEL_ACTION, GV_INT32); Map(OPT_NewMailNotify, IDC_NEW_MAIL_NOTIFY, GV_BOOL); Map(OPT_RecipientFromClipboard, IDC_POP_RECIP, GV_BOOL); Map(OPT_MarkReadAfterPreview, IDC_PREVIEW_READ, GV_BOOL); Map(OPT_MarkReadAfterSeconds, IDC_PREVIEW_SECONDS, GV_INT32); Map(OPT_AutoDeleteExe, IDC_AUTO_DELETE_EXE, GV_BOOL); Map(OPT_BlinkNewMail, IDC_BLINK, GV_BOOL); // Connection Tab Map(OPT_UseSocks, IDC_SOCKS5, GV_BOOL); Map(OPT_Socks5Server, IDC_SOCKS5_SERVER, GV_STRING); Map(OPT_Socks5UserName, IDC_SOCKS5_USER, GV_STRING); Map(OPT_Socks5Password, IDC_SOCKS5_PASSWORD, GV_STRING); Map(OPT_Pop3OnStart, IDC_POP3_ON_START, GV_BOOL); Map(OPT_Pop3DefAction, IDC_DEF_ACTION, GV_INT32); Map(OPT_HttpProxy, IDC_HTTP_PROXY, GV_STRING); Map(OPT_CheckForDialUp, IDC_CHECKDIALUP, GV_BOOL); // Appearance/Look Tab Map(OPT_EditControl, IDC_EDIT_CONTROL, GV_INT32); Map(OPT_WordWrap, IDC_WRAP, GV_BOOL); Map(OPT_WrapAtColumn, IDC_WRAP_COLS, GV_INT32); Map(OPT_DefaultAlternative, IDC_DEFAULT_ALT, GV_INT32); Map(OPT_GridLines, IDC_GRID_LINES, GV_BOOL); Map(OPT_PreviewLines, IDC_PREVIEW_LINES, GV_BOOL); Map(OPT_ToolbarText, IDC_TOOLBAR_TEXT, GV_BOOL); Map(OPT_BoldUnread, IDC_BOLD_UNREAD, GV_BOOL); Map(OPT_DateFormat, IDC_DATE_FORMAT, GV_INT32); Map(OPT_UiFontSize, IDC_UI_FNT_SIZE, GV_INT32); Map(OPT_GlyphSub, IDC_GLYPH_SUB, GV_BOOL); Map(OPT_HtmlLoadImages, IDC_HTML_IMAGES, GV_BOOL); Map(OPT_ShowFolderTotals, IDC_SHOW_TOTALS, GV_BOOL); Map(OPT_Theme, IDC_THEME, GV_STRING); Map(OPT_CalendarFirstDayOfWeek, IDC_START_WEEK); // Debug tab Map("*", IDC_ADVANCED, GV_DOM); // Processed fields LControlTree *Ct; if (GetViewById(IDC_ADVANCED, Ct)) { Ct->SetPourLargest(true); LControlTree::Item *ci = Ct->Find(OPT_LogFormat); if (ci) { LAutoPtr Enum(new LControlTree::Item::EnumArr); if (Enum) { Enum->New().Set(LLoadString(IDS_NO_LOG), NET_LOG_NONE); Enum->New().Set(LLoadString(IDS_HEX_LOG), NET_LOG_HEX_DUMP); Enum->New().Set(LLoadString(IDS_BYTE_LOG), NET_LOG_ALL_BYTES); ci->SetEnum(Enum); } } LSpellCheck *SpellThread = App->GetSpellThread(true); if (SpellThread) { if (!SpellThread->EnumLanguages(SinkHnd)) LgiTrace("%s:%i - Failed to EnumLanguages.\n", _FL); LVariant Lang; if (App->GetOptions()->GetValue(OPT_SpellCheckLanguage, Lang)) SpellThread->EnumDictionaries(SinkHnd, Lang.Str()); else LgiTrace("%s:%i - Failed to EnumDictionaries.\n", _FL); } else LgiTrace("%s:%i - Failed to get spell thread.\n", _FL); if ((ci = Ct->Find(OPT_SoftwareUpdateTime))) { LAutoPtr Enum(new LControlTree::Item::EnumArr); if (Enum) { LControlTree::EnumValue *v = &Enum->New(); v->Name = (char*)LLoadString(IDS_WEEK); v->Value = 0; v = &Enum->New(); v->Name = (char*)LLoadString(IDS_MONTH); v->Value = 1; v = &Enum->New(); v->Name = (char*)LLoadString(IDS_YEAR); v->Value = 2; ci->SetEnum(Enum); } } } else LAssert(!"GetViewById failed."); LCombo *c; if (GetViewById(IDC_THEME, c)) { LVariant CurTheme; App->GetOptions()->GetValue(OPT_Theme, CurTheme); c->Insert(LLoadString(IDS_DEFAULT)); auto Paths = ScribeThemePaths(); for (auto p: Paths) { LDirectory d; for (auto b = d.First(p); b; b = d.Next()) { if (d.IsDir()) { c->Insert(d.GetName()); if (!Stricmp(d.GetName(), CurTheme.Str())) c->Value(c->Length()-1); } } } } // Load Convert(App->GetOptions(), this, true); // Get pointers to controls if (GetViewById(IDC_DEFAULT_ALT, c)) { c->Insert("text/plain"); c->Insert("text/html"); #ifdef WINDOWS c->Insert("application/internet-explorer"); #endif } else LAssert(!"GetViewById failed."); if (GetViewById(IDC_DEF_REPLYALL_SETTING, c)) { c->Insert("To"); c->Insert("Cc"); c->Insert("Bcc"); } else LAssert(!"GetViewById failed."); if (GetViewById(IDC_EDIT_CONTROL, c)) { c->Insert("text/plain"); c->Insert("text/html (experimental)"); } else LAssert(!"GetViewById failed."); LVariant SizeAdj; if (!App->GetOptions()->GetValue(OPT_UiFontSize, SizeAdj)) { App->GetOptions()->SetValue(OPT_UiFontSize, SizeAdj = 2); } if (GetViewById(IDC_UI_FNT_SIZE, c)) { c->Insert("-2pt"); c->Insert("-1pt"); c->Insert(AutoInBrackets); c->Insert("+1pt"); c->Insert("+2pt"); } else LAssert(!"IDC_UI_FNT_SIZE missing."); if (GetViewById(IDC_UI_LANG, UiLang)) { LResources *Res = LgiGetResObj(); if (Res && Res->GetLanguages()) { LArray *InLangs = Res->GetLanguages(); for (unsigned n=0; nLength(); n++) { LLanguage *Lang = LFindLang((*InLangs)[n]); if (Lang) { Langs.Insert(Lang); } } Langs.Sort(LangCompare); int n = 0; LVariant LangId; App->GetOptions()->GetValue(OPT_UiLanguage, LangId); if (LangId.Str()) { for (auto l: Langs) { char *LocalName = Res->LanguageNames.Find(l->Id); if (LocalName && _stricmp(l->Id, "en")) { char Txt[256]; sprintf_s(Txt, sizeof(Txt), "%s (%s)", LocalName, l->Name); UiLang->Insert(Txt); } else { UiLang->Insert(l->Name); } if (_stricmp(l->Id, LangId.Str()) == 0) { UiLang->Value(n); } n++; } } } } else LAssert(!"IDC_UI_LANG missing."); if (GetViewById(IDC_START_WEEK, c)) { for (int i=0; iInsert(LDateTime::WeekdaysLong[i]); } else LAssert(!"IDC_START_WEEK missing."); if (GetViewById(IDC_DATE_FORMAT, c)) { c->Insert(AutoInBrackets); char s[256]; const char *sDay = LLoadString(IDS_DAY); const char *sMonth = LLoadString(IDS_MONTH); const char *sYear = LLoadString(IDS_YEAR); sprintf_s(s, sizeof(s), "%s/%s/%s 12h", sDay, sMonth, sYear); c->Insert(s); sprintf_s(s, sizeof(s), "%s/%s/%s 12h", sMonth, sDay, sYear); c->Insert(s); sprintf_s(s, sizeof(s), "%s/%s/%s 12h", sYear, sMonth, sDay); c->Insert(s); sprintf_s(s, sizeof(s), "%s/%s/%s 24h", sDay, sMonth, sYear); c->Insert(s); sprintf_s(s, sizeof(s), "%s/%s/%s 24h", sMonth, sDay, sYear); c->Insert(s); sprintf_s(s, sizeof(s), "%s/%s/%s 24h", sYear, sMonth, sDay); c->Insert(s); } else LAssert(!"IDC_DATE_FORMAT missing."); EditorFont.Serialize(App->GetOptions(), OPT_EditorFont, false); HtmlFont.Serialize(App->GetOptions(), OPT_HtmlFont, false); UpdateFontDescription(); LNotification note(LNotifyValueChanged); OnNotify(FindControl(IDC_REGISTER_CLIENT), note); OnNotify(FindControl(IDC_SOCKS5), note); } OptionsDlg::~OptionsDlg() { LEventSinkMap::Dispatch.RemoveSink(this); DeleteObj(d); } void OptionsDlg::UpdateDefaultSendAccounts() { LCombo *c; if (GetViewById(IDC_DEF_SEND, c)) { bool ResetDefault = false; int64 CurIdx = c->Value(); while (c->Delete((size_t)0)); int FirstValid = -1; int i = 0; for (auto a: *App->GetAccounts()) { LVariant Server = a->Send.Server(); int Disabled = a->Send.Disabled(); if (Server.Str() && !Disabled) { LVariant v = a->Send.Name(); c->Insert(v.Str()); if (FirstValid < 0) FirstValid = i; } else { c->Insert("----"); if (i == CurIdx) ResetDefault = true; } i++; } if (ResetDefault) { if (FirstValid >= 0) c->Value(FirstValid); } else c->Value(CurIdx); } } void OptionsDlg::OnAccountEnable(ScribeAccount *Acc, bool Enable) { Acc->Send.Disabled(!Enable); UpdateDefaultSendAccounts(); } bool OptionsDlg::PasswordCtrlValue(int CtrlId, char *Option, bool ToWindow) { bool Status = false; LViewI *w = FindControl(CtrlId); LVariant v; if (w && Option && App->GetOptions()) { App->GetOptions()->GetValue(Option, v); if (ToWindow) { if (v.Str()) { // option -> window // Status = w->Name(v); } } else { // window -> option const char *s = w->Name(); if (ValidStr(s)) { // user has modified the string // encrypt and store - GPassword p; + LPassword p; p.Set(s); p.Serialize(App->GetOptions(), Option, true); Status = true; } } } return Status; } void RemoveCtrl(LView *p, int i) { LViewI *v = p->FindControl(i); DeleteObj(v); } void OptionsDlg::OnCreate() { TabDialog::OnCreate(); LList *ACtrl; if (GetViewById(IDC_ACCOUNTS, ACtrl)) { ACtrl->Select(ACtrl->ItemAt(0)); } } void OptionsDlg::UpdateFontDescription() { char Str[256] = ""; if (EditorFont.GetDescription(Str, sizeof(Str))) { SetCtrlName(IDC_FONT, Str); } if (HtmlFont.GetDescription(Str, sizeof(Str))) { SetCtrlName(IDC_HTML_FONT, Str); } } void OptionsDlg::WriteNativeText(LFile &f, char *t) { #ifdef WIN32 for (char *s=t; *s; s++) { if (*s == '\n') { f.Write("\r\n", 2); } else { f << *s; } } #else f.Write(t, (int)strlen(t)); #endif } int OptionsDlg::OnNotify(LViewI *Ctrl, LNotification n) { if (!Ctrl) return 0; switch (Ctrl->GetId()) { case IDC_BROWSE_THEMES: { auto pos = Ctrl->GetPos(); LPoint pt(0, pos.Y()); Ctrl->PointToScreen(pt); LSubMenu sub; auto Paths = ScribeThemePaths(); int n = 1; for (auto p: Paths) sub.AppendItem(p, n++, true); auto res = sub.Float(Ctrl->GetWindow(), pt.x, pt.y, LSubMenu::BtnLeft); if (res) { auto browse = Paths[res-1]; FileDev->CreateFolder(browse, true); LBrowseToFile(browse); } break; } case IDC_ADVANCED: { if (n.Type == IDC_SPELL_LANG) { // If the user changes the language we have to clear the dictionary LControlTree *Ct; if (GetViewById(IDC_ADVANCED, Ct)) { LControlTree::Item *ci; if ((ci = Ct->Find(OPT_SpellCheckDictionary))) { LVariant v; ci->SetValue(v); } } } break; } case IDC_REGISTER_CLIENT: { if (Ctrl->Value()) { SetCtrlEnabled(IDC_CHECK_DEFAULT, true); } else { SetCtrlEnabled(IDC_CHECK_DEFAULT, false); SetCtrlValue(IDC_CHECK_DEFAULT, false); } break; } case IDC_LAUNCH_HELP: { char Path[256] = "install.html"; switch (GetCtrlValue(IDC_TAB)) { case 0: // identity strcat_s(Path, sizeof(Path), "#id"); break; case 1: // accounts strcat_s(Path, sizeof(Path), "#accounts"); break; case 2: // general strcat_s(Path, sizeof(Path), "#general"); break; case 3: // connection strcat_s(Path, sizeof(Path), "#connect"); break; case 4: // appearence strcat_s(Path, sizeof(Path), "#appear"); break; case 5: // other strcat_s(Path, sizeof(Path), "#other"); break; } App->LaunchHelp(Path); break; } case IDC_SOCKS5: { bool SocksOn = Ctrl->Value() != 0; SetCtrlEnabled(IDC_SOCKS5_SERVER, SocksOn); SetCtrlEnabled(IDC_SOCKS5_USER, SocksOn); SetCtrlEnabled(IDC_SOCKS5_PASSWORD, SocksOn); break; } case IDC_EDIT_REPLY: { new UtfEditor(App, OPT_TextReplyFormat, OPT_HtmlReplyFormat, LLoadString(IDS_REPLY)); break; } case IDC_RESET_REPLY: { LVariant v; App->GetOptions()->SetValue(OPT_TextReplyFormat, v = DefaultTextReplyTemplate); App->GetOptions()->SetValue(OPT_HtmlReplyFormat, v = DefaultHtmlReplyTemplate); break; } case IDC_EDIT_FORWARD: { new UtfEditor(App, OPT_TextForwardFormat, OPT_HtmlForwardFormat, LLoadString(IDS_FORWARD)); break; } case IDC_RESET_FORWARD: { LVariant v; App->GetOptions()->SetValue(OPT_TextForwardFormat, v = DefaultTextReplyTemplate); App->GetOptions()->SetValue(OPT_HtmlForwardFormat, v = DefaultHtmlReplyTemplate); break; } case IDC_ACCOUNTS: { if (n.Type == LNotifyItemInsert || n.Type == LNotifyItemDelete) { UpdateDefaultSendAccounts(); } break; } case IDC_ADD: { - App->GetAccountSettingsAccess(this, ScribeWriteAccess, [&](auto Allow) + App->GetAccountSettingsAccess(this, ScribeWriteAccess, [this](auto Allow) { if (!Allow) return; List *AList = App->GetAccounts(); LList *ACtrl; if (AList && GetViewById(IDC_ACCOUNTS, ACtrl)) { int Items = (int)AList->Length(); - LAutoPtr a(new ScribeAccount(App, Items)); + ScribeAccount *a = new ScribeAccount(App, Items); if (a) { a->Create(); // Open the UI - a->InitUI(this, 0, [&](auto status) + a->InitUI(this, 0, [this, ACtrl, AList, a](auto status) { if (status) { ACtrl->Insert(new AccountItem(this, a)); - AList->Insert(a.Release()); + AList->Insert(a); UpdateDefaultSendAccounts(); } + else delete a; }); } } }); break; } case IDC_DELETE: { - App->GetAccountSettingsAccess(this, ScribeWriteAccess, [&](auto Allow) + App->GetAccountSettingsAccess(this, ScribeWriteAccess, [this](auto Allow) { if (!Allow) return; List Sel; LList *ACtrl; if (GetViewById(IDC_ACCOUNTS, ACtrl) && ACtrl->GetSelection(Sel)) { List *AList = App->GetAccounts(); // For all selected for (auto i: Sel) { ScribeAccount *a = i->GetAccount(); if (a->IsOnline()) { LgiMsg( this, "You can't delete the account while it's still active.", AppName); } else { if (AList) { // Delete the applications reference AList->Delete(a); } a->Delete(); delete a; // delete actual account object } } { // Reindex remaining items so their are no gaps int i=0; for (auto a: *AList) { a->ReIndex(i++); } } } }); break; } case IDC_UP: case IDC_DOWN: { - App->GetAccountSettingsAccess(this, ScribeWriteAccess, [&](auto Allow) + App->GetAccountSettingsAccess(this, ScribeWriteAccess, [this, Ctrl](auto Allow) { if (!Allow) return; List a; LList *ACtrl; if (!GetViewById(IDC_ACCOUNTS, ACtrl) || !ACtrl->GetAll(a)) return; AccountItem *Sel = NULL; for (int i=0; iSelect()) { Sel = a[i]; break; } } if (!Sel) return; bool IsUp = Ctrl->GetId() == IDC_UP; int Idx = ACtrl->IndexOf(Sel); int NewIdx = IsUp ? Idx - 1 : Idx + 1; if (NewIdx < 0) return; ACtrl->Remove(Sel); ACtrl->Insert(Sel, NewIdx); Sel->Select(true); ACtrl->GetAll(a); for (int i=0; iGetAccount(); if (Acc) Acc->Identity.Sort(i+1); } ACtrl->Sort(AccountCmp); }); break; } case IDC_PROPERTIES: { - App->GetAccountSettingsAccess(this, ScribeReadAccess, [&](auto Allow) + App->GetAccountSettingsAccess(this, ScribeReadAccess, [this](auto Allow) { if (!Allow) return; List Sel; LList *ACtrl; if (GetViewById(IDC_ACCOUNTS, ACtrl) && ACtrl->GetSelection(Sel)) { AccountItem *i = dynamic_cast(Sel[0]); if (i) { - i->GetAccount()->InitUI(this, 0, [&](auto status) + i->GetAccount()->InitUI(this, 0, [this, i](auto status) { if (status) { i->Update(); UpdateDefaultSendAccounts(); } }); } } else { LgiMsg(this, "No account selected.", AppName, MB_OK); } }); break; } case IDC_SET_SOUND: { auto Select = new LFileSelect(this); Select->Type("Sound", "*.wav"); Select->Open([this](auto dlg, auto status) { if (status) { LEdit *LogFile; if (GetViewById(IDC_NEW_MAIL_SOUND, LogFile)) LogFile->Name(dlg->Name()); } delete dlg; }); break; } case IDC_SET_FONT: { - EditorFont.DoUI(this, [&](auto fontType) + EditorFont.DoUI(this, [this](auto fontType) { UpdateFontDescription(); }); break; } case IDC_SET_HTML_FONT: { - HtmlFont.DoUI(this, [&](auto fontType) + HtmlFont.DoUI(this, [this](auto fontType) { UpdateFontDescription(); }); break; } case IDC_CONFIGURE_REMOTE_CONTENT: { auto Dlg = new RemoteContentDlg(this, App); Dlg->DoModal(NULL); break; } case IDOK: { LOptionsFile *Opts = App->GetOptions(); // Font EditorFont.Serialize(Opts, OPT_EditorFont, true); HtmlFont.Serialize(Opts, OPT_HtmlFont, true); if (UiLang) { LLanguage *Lang = Langs[(int)UiLang->Value()]; if (Lang) { LVariant v; Opts->SetValue(OPT_UiLanguage, v = Lang->Id); } else { Opts->DeleteValue(OPT_UiLanguage); } } LList *ACtrl; if (GetViewById(IDC_ACCOUNTS, ACtrl)) { List a; ACtrl->GetAll(a); for (auto ai: a) { ai->Account->Send.Disabled(ai->Disable->Value() != 0); } } LVariant Cur[2], New[2], v; Opts->GetValue(OPT_SpellCheckLanguage, Cur[0]); Opts->GetValue(OPT_SpellCheckDictionary, Cur[1]); Convert(App->GetOptions(), this, false); Opts->GetValue(OPT_SpellCheckLanguage, New[0]); Opts->GetValue(OPT_SpellCheckDictionary, New[1]); if (Cur[0] != New[0] || Cur[1] != New[1]) { LgiTrace("%s:%i - OnSpellerSettingChange: Lang(%s), Dict(%s)\n", _FL, New[0].Str(), New[1].Str()); App->OnSpellerSettingChange(); } if (Opts->GetValue(OPT_SizeInKiB, v)) OptionSizeInKiB = v.CastInt32() != 0; if (Opts->GetValue(OPT_RelativeDates, v)) ShowRelativeDates = v.CastInt32() != 0; CalendarViewWnd::OnOptionsChange(); EndModal(1); break; } case IDCANCEL: { EndModal(0); break; } } return 0; } LMessage::Result OptionsDlg::OnEvent(LMessage *m) { switch (m->Msg()) { case M_ENUMERATE_LANGUAGES: { LControlTree *Ct; if (!GetViewById(IDC_ADVANCED, Ct)) { LgiTrace("%s:%i - No control tree.\n", _FL); break; } LAutoPtr< LArray > Langs((LArray*)m->A()); if (!Langs) { LgiTrace("%s:%i - Error: No dictionary list.\n", _FL); break; } LControlTree::Item *ci; if (!(ci = Ct->Find(OPT_SpellCheckLanguage))) { LgiTrace("%s:%i - Error: No OPT_SpellCheckLanguage leaf.\n", _FL); break; } LAutoPtr Enum(new LControlTree::Item::EnumArr); if (!Enum) { LgiTrace("%s:%i - Error: alloc failed.\n", _FL); break; } for (unsigned i=0; iLength(); i++) { LSpellCheck::LanguageId &p = (*Langs)[i]; LControlTree::EnumValue &e = Enum->New(); if (p.EnglishName && p.NativeName) { LString s; s.Printf("%s / %s", p.NativeName.Get(), p.EnglishName.Get()); e.Name = s; } else if (p.EnglishName) e.Name = p.EnglishName; else if (p.LangCode) e.Name = p.LangCode; else LAssert(!"Null object."); e.Value = p.EnglishName ? p.EnglishName : p.LangCode; } ci->SetEnum(Enum); break; } case M_ENUMERATE_DICTIONARIES: { LControlTree *Ct; if (!GetViewById(IDC_ADVANCED, Ct)) break; LAutoPtr< LArray > Dicts((LArray*)m->A()); if (!Dicts) { LgiTrace("%s:%i - Error: No dictionary list.\n", _FL); break; } LControlTree::Item *ci; if (!(ci = Ct->Find(OPT_SpellCheckDictionary))) { LgiTrace("%s:%i - Error: No OPT_SpellCheckDictionary leaf.\n", _FL); break; } LAutoPtr Enum(new LControlTree::Item::EnumArr); if (!Enum) { LgiTrace("%s:%i - Error: alloc failed.\n", _FL); break; } for (unsigned i=0; iLength(); i++) { LSpellCheck::DictionaryId &s = (*Dicts)[i]; LControlTree::EnumValue &e = Enum->New(); e.Name = s.Dict; e.Value = s.Dict; } ci->SetEnum(Enum); break; } } return TabDialog::OnEvent(m); } ///////////////////////////////////////////////////////////////////////////// UtfEditor::UtfEditor(ScribeWnd *app, const char *txtopt, const char *htmlopt, const char *desc) { Txt = NULL; Html = NULL; TabText = TabHtml = 0; LRect r(0, 0, 600, 500); SetPos(r); MoveToCenter(); App = app; TextOpt = txtopt; HtmlOpt = htmlopt; Cancel = Ok = 0; if (Attach(0)) { Children.Insert(new LTextLabel(-1, 5, 5, -1, -1, desc)); Children.Insert(Tabs = new LTabView(200, 0, 30, 300, 300)); if (Tabs) { Tabs->SetPourLargest(false); Tabs->SetPourChildren(true); if ((TabText = Tabs->Append("Text"))) { TabText->Append(Txt = new LTextView3(IDC_TEXT_VIEW, 0, 0, 100, 100)); Txt->SetPourLargest(true); Txt->Sunken(true); } if ((TabHtml = Tabs->Append("Html"))) { TabHtml->Append(Html = new LRichTextEdit(IDC_HTML_VIEW, 0, 0, 100, 100)); Html->SetPourLargest(true); Html->Sunken(true); } } Children.Insert(Ok = new LButton(IDOK, 5, 5, 75, 20, LLoadString(IDS_OK))); Children.Insert(Cancel = new LButton(IDCANCEL, 75, 5, 75, 20, LLoadString(IDS_CANCEL))); LVariant s; if (Txt && App->GetOptions()->GetValue(TextOpt, s)) Txt->Name(s.Str()); if (Html && App->GetOptions()->GetValue(HtmlOpt, s)) Html->Name(s.Str()); AttachChildren(); OnPosChange(); Visible(true); } } void UtfEditor::OnPosChange() { if (Cancel) { LRect c = GetClient(); LRect r = Cancel->GetPos(); r.Offset(c.X() - Cancel->X() - 5 - r.x1, 0); Cancel->SetPos(r); r = Ok->GetPos(); r.Offset(Cancel->GetPos().x1 - Ok->X() - 5 - r.x1, 0); Ok->SetPos(r); r = Tabs->GetPos(); r.x2 = c.x2; r.y2 = c.y2; Tabs->SetPos(r); } } int UtfEditor::OnNotify(LViewI *c, LNotification n) { switch (c->GetId()) { case IDOK: { LVariant v; LOptionsFile *Opts = App->GetOptions(); if (Txt) Opts->SetValue(TextOpt, v = Txt->Name()); if (Html) { const char *n = Html->Name(); Opts->SetValue(HtmlOpt, v = n); } // fall thru } case IDCANCEL: { Quit(); break; } } return 0; } diff --git a/Code/PrintPreview.cpp b/Code/PrintPreview.cpp --- a/Code/PrintPreview.cpp +++ b/Code/PrintPreview.cpp @@ -1,259 +1,259 @@ #include "Scribe.h" #include "PrintPreview.h" #include "resdefs.h" #include "lgi/common/ZoomView.h" #include "lgi/common/TableLayout.h" #include "lgi/common/Slider.h" #include "lgi/common/FileSelect.h" #define DEFAULT_PAGE_SIZE 1000 struct PrintPreviewPriv { ScribeWnd *App = NULL; LPrintDC *PrintDC = NULL; Mail *m = NULL; PrintPreview *Wnd = NULL; LZoomView *Zoom = NULL; LAutoPtr Mem; LAutoPtr Ctrl; bool IsLoaded = false; int HeaderContentHeight = 0; LSlider *Slider = NULL; bool InUpdate = false; LString Ranges; PrintPreviewPriv(PrintPreview *w) { Wnd = w; if (Ctrl.Reset(new Html1::LHtml(IDC_HTML_IMAGES, 0, 0, DEFAULT_PAGE_SIZE, 4000))) { Ctrl->SetMaxPaintTime(5000); Ctrl->SetLoadImages(true); } } bool Render() { // Create a HTML control if (!Zoom) Wnd->GetViewById(IDC_PREVIEW, Zoom); if (!Ctrl || !Zoom) { LgiTrace("%s:%i - Failed to create controls.\n", _FL); return false; } // Set width int Width = (int)Wnd->GetCtrlValue(IDC_WIDTH); LRect p(0, 0, Width-1, 100000); Ctrl->SetPos(p); // Get the size of the HTML layout LPoint Size = Ctrl->Layout(); if (Size.x <= 0 || Size.y <= 0) { LgiTrace("%s:%i - No content to print.\n", _FL); return false; } // Create a memory context big enough for all the content if (!Mem || Mem->X() < Width || Mem->Y() < Size.y) { Zoom->SetSurface(NULL, true); if (!Mem.Reset(new LMemDC(Width, Size.y, System24BitColourSpace))) { LgiTrace("%s:%i - Can't create memory bitmap context (%ix%i).\n", _FL, Width, Size.y); return false; } } // Clear the page to white Mem->Colour(LColour::White); Mem->Rectangle(); // Ask the HTML control to paint itself into the memory context Ctrl->OnPaint(Mem); if (Ctrl->GetMaxPaintTimeout()) { LAssert(!"Max paint time reached."); } Zoom->SetDefaultZoomMode(LZoomView::ZoomFitX); Zoom->SetSurface(Mem, false); return true; } }; PrintPreview::PrintPreview(ScribeWnd *App, Mail *m, LPrintDC *PrintDC) { d = new PrintPreviewPriv(this); d->App = App; d->m = m; d->PrintDC = PrintDC; if (!LoadFromResource(IDD_PRINT_PREVIEW)) { LAssert(!"Resource missing."); return; } ThingUi *Ui = m->GetUI(); MoveSameScreen(Ui ? (LWindow*)Ui : App); SetCtrlValue(IDC_WIDTH, DEFAULT_PAGE_SIZE); SetCtrlValue(IDC_SLIDE, DEFAULT_PAGE_SIZE); SetCtrlName(IDC_STATUS, LLoadString(IDS_LOADING)); SetCtrlValue(IDC_PAGE_ALL, true); if (GetViewById(IDC_SLIDE, d->Slider)) d->Slider->SetRange(LRange(500, 1500)); // Give it the HTML to parse d->Ctrl->SetNotify(this); d->Ctrl->Visible(false); AddView(d->Ctrl); LAutoString HtmlContent(NewStr(m->GetHtml())); d->Ctrl->SetEnv(m); d->Ctrl->SetCharset(m->GetHtmlCharset()); d->Ctrl->Name(HtmlContent); d->Render(); SetPulse(300); } PrintPreview::~PrintPreview() { delete d; } void PrintPreview::OnPulse() { if (d->Ctrl) { // This is basically a hack to get images working in the HTML control. // As it's not connected to a View heirachy it can't send messages to // itself like normal. LMessage m(M_JOBS_LOADED); d->Ctrl->OnEvent(&m); } } LSurface *PrintPreview::GetImage() { return d->Mem; } LAutoPtr PrintPreview::ReleaseImage() { return d->Mem; } LString PrintPreview::GetPageRanges() { return d->Ranges; } void PrintPreview::OnPosChange() { auto it = Children.begin(); LLayout *t = dynamic_cast((LViewI*)it); if (t) { LRect r = GetClient(); r.Inset(LTableLayout::CellSpacing, LTableLayout::CellSpacing); t->SetPos(r); } } int PrintPreview::OnNotify(LViewI *Ctrl, LNotification n) { switch (Ctrl->GetId()) { case IDC_PAGE_RANGES: { bool HasContent = Strlen(Ctrl->Name()) > 0; SetCtrlValue(IDC_PAGE_PARTIAL, HasContent); SetCtrlValue(IDC_PAGE_ALL, !HasContent); break; } case IDC_SLIDE: { if (!d->InUpdate) { d->InUpdate = true; SetCtrlValue(IDC_WIDTH, Ctrl->Value()); d->InUpdate = false; } break; } case IDC_WIDTH: { if (!d->InUpdate) { d->InUpdate = true; SetCtrlValue(IDC_SLIDE, Ctrl->Value()); d->InUpdate = false; } break; } case IDC_UPDATE: { d->Zoom->SetSurface(NULL, false); d->Mem.Reset(); SetCtrlName(IDC_STATUS, LLoadString(IDS_LOADING)); d->Render(); SetCtrlName(IDC_STATUS, LLoadString(IDS_PREVIEW)); break; } case IDC_SAVE_IMG: { if (d->Mem) { auto s = new LFileSelect(this); s->Type("JPEG", "*.jpg"); char p[MAX_PATH_LEN]; LGetSystemPath(LSP_USER_DOWNLOADS, p, sizeof(p)); LMakePath(p, sizeof(p), p, "print-preview.jpg"); s->Name(p); - s->Save([&](auto dlg, auto status) + s->Save([this](auto s, auto ok) { - if (status) + if (ok) GdcD->Save(s->Name(), d->Mem); - delete dlg; + delete s; }); } else LgiMsg(this, "No image to save.", LLoadString(IDS_ERROR)); break; } case IDOK: { auto Partial = GetCtrlValue(IDC_PAGE_PARTIAL); d->Ranges = Partial ? GetCtrlName(IDC_PAGE_RANGES) : NULL; EndModal(1); break; } case IDCANCEL: { EndModal(0); break; } case IDC_HTML_IMAGES: { if (n.Type == LNotifyDocLoaded) { // Html control finished loading... d->IsLoaded = true; d->Render(); SetCtrlEnabled(IDOK, true); SetCtrlName(IDC_STATUS, LLoadString(IDS_PREVIEW)); } break; } } return 0; } diff --git a/Code/RemoteCalendarSource.cpp b/Code/RemoteCalendarSource.cpp --- a/Code/RemoteCalendarSource.cpp +++ b/Code/RemoteCalendarSource.cpp @@ -1,426 +1,439 @@ #include "Scribe.h" #include "Calendar.h" #include "CalendarView.h" #include "resdefs.h" #include "lgi/common/EventTargetThread.h" #include "lgi/common/Http.h" #include "lgi/common/vCard-vCal.h" enum Msgs { M_LOAD_URI = M_USER + 1000, M_LOADED }; struct RemoteCalendarSourcePriv : public LEventTargetThread { RemoteCalendarSource *Source; LString Uri; LString Name; bool Error = false; bool Loaded = false; LArray Events; RemoteCalendarSourcePriv(RemoteCalendarSource *src) : Source(src), LEventTargetThread("RemoteCalendarSourcePriv") { } ~RemoteCalendarSourcePriv() { for (auto c: Events) c->DecRef(); } void Post(int m, LMessage::Param a = 0, LMessage::Param b = 0) { auto app = Source->GetApp(); app->PostEvent( M_CALENDAR_SOURCE_EVENT, (LMessage::Param)Source, (LMessage::Param)new LMessage(m, a, b)); } LMessage::Result OnEvent(LMessage *Msg) { switch (Msg->Msg()) { case M_LOAD_URI: { LString err; LStringPipe out; auto r = LgiGetUri(this, &out, &err, Uri); if (r) { /* auto s = out.NewLStr(); LgiTrace("s='%s'\n", s.Get()); */ VCal imp; while (true) { Calendar *c = new Calendar(Source->GetApp()); // LgiTrace("outsize=" LPrintfInt64 "\n", out.GetSize()); if (imp.Import(c->GetObject(), &out)) Events.Add(c); else { c->DecRef(); break; } } Post(M_LOADED); } break; } } return 0; } }; RemoteCalendarSource::RemoteCalendarSource(ScribeWnd *a, const char *id) { d = new RemoteCalendarSourcePriv(this); App = a; Id = id; } RemoteCalendarSource::~RemoteCalendarSource() { DeleteObj(d); } const char *RemoteCalendarSource::GetUri() { return d->Uri; } void RemoteCalendarSource::SetUri(const char *uri) { d->Uri = uri; Write(); OnChange(false); } bool RemoteCalendarSource::Read() { if (!Id) return false; LString k = GetKey(); LXmlTag *t = App->GetOptions()->LockTag(k, _FL); if (!t) return false; char *Col = t->GetAttr("Colour"); if (Col) Colour.Set((uint32_t)atoi64(Col), 32); else Colour.Empty(); d->Uri = t->GetAttr(CalendarSource::OptUri); auto c = Atoi(t->GetAttr(CalendarSource::OptColour)); if (c >= 0) Colour.c32((uint32_t)c); else Colour.Empty(); Display = t->GetAsInt(CalendarSource::OptDisplay); App->GetOptions()->Unlock(); OnChange(false); return true; } bool RemoteCalendarSource::Write() { LVariant v; if (!Id) { LXmlTag *t = App->GetOptions()->LockTag(OPT_CalendarSources, _FL); if (t) { LString Key; for (int i=0; i<100; i++) { Key.Printf("Source-%i", LRand(10000)); if (!t->GetChildTag(Key)) { Id = Key; break; } } App->GetOptions()->Unlock(); } } if (!Id) return false; auto Key = GetKey(); auto t = App->GetOptions()->LockTag(Key, _FL); if (!t) { App->GetOptions()->CreateTag(Key); t = App->GetOptions()->LockTag(Key, _FL); } if (!t) return false; SaveAttr(t, CalendarSource::OptUri, d->Uri); t->SetAttr(CalendarSource::OptColour, (int64_t) Colour.c32()); t->SetAttr(CalendarSource::OptDisplay, Display); t->SetAttr(CalendarSource::OptObject, GetClass()); App->GetOptions()->Unlock(); return true; } bool RemoteCalendarSource::Delete() { LString k = GetKey(); bool r = App->GetOptions()->DeleteTag(k); if (r) App->SaveOptions(); else LAssert(!"Delete failed."); return r; } Calendar *RemoteCalendarSource::NewEvent() { // Can't create remote events... read only feed. return NULL; } bool RemoteCalendarSource::Match(char *Email) { return false; } -bool RemoteCalendarSource::GetEvents(LDateTime &StartTs, LDateTime &EndTs, LArray &Events) +bool RemoteCalendarSource::GetEvents(const LDateTime StartTs, + const LDateTime EndTs, + std::function&)> Callback) { + if (!Callback) + return false; + + LArray Events; if (!Display) + { + Callback(Events); return false; + } if (!d->Loaded) { d->Loaded = true; d->PostEvent(M_LOAD_URI); + + // FIXME: Should call the callback when loaded...? + Callback(Events); return true; } LDateTime Start = StartTs; Start.ToUtc(); LDateTime End = EndTs; End.ToUtc(); for (auto c: d->Events) { LDateTime s, e; if (c->GetCalType() == CalEvent && c->GetField(FIELD_CAL_START_UTC, s)) { int Recur = 0; c->GetField(FIELD_CAL_RECUR, Recur); const char *Sub = NULL; c->GetField(FIELD_CAL_SUBJECT, Sub); if (Recur) { LArray Times; if (c->GetTimes(Start, End, Times)) { SetCalendarsSource(c); for (auto &t: Times) { t.src = this; Events.Add(t); } } } else { if (!c->GetField(FIELD_CAL_END_UTC, e)) { e = s; e.AddHours(1); } #if 0 printf("%s: %s > %s, %s < %s\n", Sub, s.Get().Get(), End.Get().Get(), e.Get().Get(), Start.Get().Get()); #endif if (s > End || e < Start) { // Is before/after the range } else { TimePeriod &tp = Events.New(); tp.src = this; tp.c = c; tp.s = s; tp.e = e; tp.ToLocal(); SetCalendarsSource(c); } } } } - return false; + Callback(Events); + return true; } void RemoteCalendarSource::EditPath(LView *parent, CalendarView *cv) { auto Dlg = new LInput(parent, d->Uri); Dlg->DoModal([this, Dlg, cv](auto dlg, auto id) { if (id) { SetUri(Dlg->GetStr()); if (cv) cv->OnContentsChanged(this); } delete dlg; }); } LColour RemoteCalendarSource::GetColour() { return Colour; } void RemoteCalendarSource::SetColour(LColour c) { Colour = c; } const char *RemoteCalendarSource::GetName() { return d->Name; } void RemoteCalendarSource::OnMouseClick(LMouse &m) { if (m.IsContextMenu()) { } else if (m.Down() && m.Left() && Parent) { int Col = Parent->ColumnAtX(m.x); if (Col == 0) { Display = !Display; Update(); Parent->SendNotify(LNotifyValueChanged); } else if (Col > 0) { SetCreateIn(this); } } } void RemoteCalendarSource::OnPaintColumn(LItem::ItemPaintCtx &Ctx, int i, LItemColumn *c) { if (i == 0) { LRect r = Ctx; Ctx.pDC->Colour(Ctx.Back); for (int i=0; i<4; i++) { Ctx.pDC->Box(&r); r.Inset(1, 1); } Ctx.pDC->Colour(Colour); if (Display) Ctx.pDC->Rectangle(&r); else { Ctx.pDC->Box(&r); r.Inset(1, 1); Ctx.pDC->Colour(Ctx.Back); Ctx.pDC->Rectangle(&r); } } else { bool PathErr = (i == 1 && d->Error); if (PathErr) Ctx.Fore = LColour::Red; LListItem::OnPaintColumn(Ctx, i, c); if (PathErr) { Ctx.pDC->Colour(Ctx.Fore); int Cy = Ctx.y1 + (Ctx.Y() >> 1) + 1; Ctx.pDC->Line(Ctx.x1, Cy, Ctx.x2, Cy); } } } const char *RemoteCalendarSource::GetText(int i) { if (i == 1) return d->Uri; return NULL; } void RemoteCalendarSource::OnFolderDelete(ScribeFolder *f) { } void RemoteCalendarSource::OnPulse() { } void RemoteCalendarSource::OnChange(bool IsDelete) { Update(); if (!GetList()) return; auto w = GetList()->GetWindow(); if (!w) return; CalendarView *cv = NULL; if (!w->GetViewById(IDC_CALENDAR, cv)) return; if (IsDelete) cv->OnSourceDelete(this); else cv->OnContentsChanged(this); } LMessage::Result RemoteCalendarSource::OnEvent(LMessage *Msg) { switch (Msg->Msg()) { case M_LOADED: { OnChange(false); break; } } return 0; } diff --git a/Code/ReplicateDlg.cpp b/Code/ReplicateDlg.cpp --- a/Code/ReplicateDlg.cpp +++ b/Code/ReplicateDlg.cpp @@ -1,1447 +1,1447 @@ #include "Scribe.h" #include "ReplicateDlg.h" #include "lgi/common/Combo.h" #include "lgi/common/List.h" #include "resdefs.h" #include "lgi/common/ListItemCheckBox.h" #include "lgi/common/ProgressDlg.h" #include "lgi/common/LgiRes.h" #define SECONDS * 1000 #define REPLICATE_TIMEOUT (30 SECONDS) #define REPLICATE_TRANS_LENGTH 10 #define MAX_DELAYED_UNITS 10 #define DEBUG_LOGGING 1 enum WorkType { RNull, RCountFinish, RCountSource, // Uses 'Folder1' RDeleteFolder, // Uses 'Folder1' RCopySubFolder, // Uses 'Folder2' RCopyFolder, // Uses 'Folder2' RCreateFolder, // Uses 'Folder2' RCopyData, // Uses 'Data' RSaveData, // Uses 'Data' RReloadFolders, // Tells 'App' to load folders.. ROpenMailStore, // Uses 'Open' RCopyStore, // Expects 'SrcStore' and 'DstStore' as arguments REndTransaction, }; const char *ToString(WorkType t) { switch (t) { case RNull: return "RNull"; case RCountSource: return "RCountSource"; case RCountFinish: return "RCountFinish"; case RCopyFolder: return "RCopyFolder"; case RCopySubFolder: return "RCopySubFolder"; case RCreateFolder: return "RCreateFolder"; case RCopyData: return "RCopyData"; case RSaveData: return "RSaveData"; case RReloadFolders: return "RReloadFolders"; case ROpenMailStore: return "ROpenMailStore"; case RCopyStore: return "RCopyStore"; case REndTransaction: return "REndTransaction"; case RDeleteFolder: return "RDeleteFolder"; } return "#error"; } static LString ObjToString(LDataI *d) { LString s; switch ((unsigned)d->Type()) { case MAGIC_MAIL: s = d->GetStr(FIELD_MESSAGE_ID); break; case MAGIC_CONTACT: s.Printf("%s %s", d->GetStr(FIELD_FIRST_NAME), d->GetStr(FIELD_LAST_NAME)); break; case MAGIC_FILTER: s = d->GetStr(FIELD_FILTER_NAME); break; case MAGIC_CALENDAR: s = d->GetStr(FIELD_CAL_SUBJECT); break; case MAGIC_GROUP: s = d->GetStr(FIELD_GROUP_NAME); break; default: s.Printf("Error: Unknown type '%x'", d->Type()); break; } return s; } struct ScribeReplicator : public LProgressDlg, public LDataEventsI { bool CanRunConcurrent(WorkType t) { return t == RCopyData || t == RSaveData; } struct CopyStatus { int Total; int Errors; int Ok; CopyStatus() { Total = 0; Errors = 0; Ok = 0; } }; LHashTbl, CopyStatus*> Status; struct WorkUnit { WorkType Type; uint64 Ts; bool Delayed; LArray Deferred; union { struct { LDataFolderI *Src, *Dst; } Folder2; // Valid if Type == RCopyFolder struct { LDataFolderI *Folder; } Folder1; // Valid if Type == RCountSource struct { LDataFolderI *DstFld; LDataFolderI *SrcFld; LDataI *Src; } Data; // Valid if Type == RCopyData struct { ReplicateDlg::AccountSpec *Spec; LAutoPtr *Store; } Open; // Valid if Type == ROpenMailStore }; LString ToString() { LString s; switch (Type) { // Folder2 users case RCopyFolder: case RCopySubFolder: case RCreateFolder: s.Printf("%s: %s <- %s", ::ToString(Type), Folder2.Dst->GetStr(FIELD_FOLDER_NAME), Folder2.Src->GetStr(FIELD_FOLDER_NAME)); break; // Folder1 users case RCountSource: case RDeleteFolder: s.Printf("%s: %s", ::ToString(Type), Folder1.Folder->GetStr(FIELD_FOLDER_NAME)); break; // Data users case RCopyData: case RSaveData: s.Printf("%s: %s <- %s", ::ToString(Type), Data.DstFld->GetStr(FIELD_FOLDER_NAME), ObjToString(Data.Src).Get()); break; default: s = ::ToString(Type); break; } return s; } }; ScribeWnd *App; int Folders, Items; bool Types[MAGIC_MAX-MAGIC_BASE]; int Copied[MAGIC_MAX-MAGIC_BASE]; bool Recurse; bool DeleteSourceOnSuccess; LArray Work; LAutoPtr SrcStore, DstStore; ReplicateDlg::AccountSpec SrcSpec, DstSpec; LDataStoreI::StoreTrans Trans; int TransLen; bool RestartOnPulse; bool PulseStarted; uint64 LastEvent; int UnitsTimedOut; LString OverviewMsg; LString StatusMsg; // Error handling.. int FailedWork; LStringPipe ErrorLog; ScribeReplicator(ScribeWnd *app) : LProgressDlg(app) { App = app; Recurse = true; DeleteSourceOnSuccess = false; LastEvent = 0; Folders = 0; Items = 0; UnitsTimedOut = 0; PulseStarted = false; RestartOnPulse = false; FailedWork = 0; ZeroObj(Copied); SetDescription("Loading..."); App->OnFolderTask(this, true); App->AddStore3EventHandler(this); SetAlwaysOnTop(true); } ~ScribeReplicator() { App->OnFolderTask(this, false); App->RemoveStore3EventHandler(this); Status.DeleteObjects(); } LMessage::Result OnEvent(LMessage *Msg) { switch (Msg->Msg()) { case M_REPLICATE_NEXT: return DoNext(); case M_STORAGE_EVENT: { LDataStoreI *Store = (LDataStoreI*)Msg->A(); if (Store) Store->OnEvent((void*)Msg->B()); break; } } return LProgressDlg::OnEvent(Msg); } LString ToString() { LString sep("\n"); LString::Array s; for (unsigned i=0; iGetStore()->StartTransaction(); TransLen = 0; } // This is called after there is a valid object to save. // It may end up with a deferred save. int SaveObject(WorkUnit *w) { LDataFolderI *df = w->Data.DstFld; LDataFolderI *sf = w->Data.SrcFld; LDataI *s = w->Data.Src; LDataI *d = df->GetStore()->Create(s->Type()); if (d) { d->CopyProps(*s); Store3Status Result = d->Save(df); if (Result == Store3Error) { ErrorLog.Print("%s:%i - Failed to save item.\n", _FL); } else if (Result == Store3Delayed) { w->Type = RSaveData; w->Data.Src = d; w->Delayed = true; w->Ts = LCurrentTime(); return true; } CopyStatus *Cs = Status.Find(sf); if (Cs) Cs->Ok++; TransLen++; } else ErrorLog.Print("%s:%i - Failed to create object of type %i\n", _FL, s->Type()); Pop(w); Value(Value() + 1); if (TransLen >= REPLICATE_TRANS_LENGTH) { Trans.Reset(); } return true; } void Pop(WorkUnit *w) { if (Work.PtrCheck(w)) { ptrdiff_t Idx = w - &Work.First(); Work.DeleteAt((int)Idx, true); } else LAssert(0); } int DoNext() { if (Work.Length() == 0) return OnFinish(); if (!PulseStarted) { SetPulse(60); PulseStarted = true; } uint64 Start = LCurrentTime(); LastEvent = Start; WorkUnit *w = NULL; int DelayedCopies = 0, DelayedSaves = 0, DelayedCreate = 0; for (unsigned i=0; iDelayed) { if (w->Type == RCopyData) DelayedCopies++; else if (w->Type == RSaveData) DelayedSaves++; else if (w->Type == RCreateFolder) DelayedCreate++; } } if (DelayedCopies + DelayedSaves >= MAX_DELAYED_UNITS || DelayedCreate > 0) { RestartOnPulse = true; return true; } for (ssize_t i=(ssize_t)Work.Length()-1; i>=0; i--) { w = &Work[i]; if (CanRunConcurrent(w->Type)) { if (w->Delayed) continue; // Find the last undelayed unit // else do this unit } else { if (w->Delayed) w = NULL; // We wait for it to complete.. // else do this unit } break; } if (!w) { RestartOnPulse = true; #if DEBUG_LOGGING LgiTrace("%s:%i - DoNext all remaining delayed (%i/%i)\n", _FL, DelayedCopies, DelayedSaves); #endif return true; } #if DEBUG_LOGGING LString Str = w->ToString(); LgiTrace("Do: %s\n", Str.Get()); #endif switch (w->Type) { case RCreateFolder: { // Just wait for it to be created... LAssert(w->Delayed); RestartOnPulse = true; return true; } case RDeleteFolder: { LDataFolderI *f = w->Folder1.Folder; if (!f) { ErrorLog.Print("%s:%i - Invalid folder.\n", _FL); Pop(w); break; } // Get the copy status CopyStatus *Cs = Status.Find(f); if (!Cs) { ErrorLog.Print("%s:%i - No copy status for '%s'.\n", _FL, f->GetStr(FIELD_FOLDER_NAME)); Pop(w); break; } // Check that all the items have been copied across. if (Cs->Ok + Cs->Errors < Cs->Total) { // Go into wait mode... RestartOnPulse = true; return true; } if (Cs->Total != Cs->Ok) { // Error out if not ErrorLog.Print("%s:%i - Delete '%s' skipped: %i of %i ok (%i errors).\n", _FL, f->GetStr(FIELD_FOLDER_NAME), Cs->Ok, Cs->Total, Cs->Errors); Pop(w); break; } Store3Status s = f->Delete(); if (s == Store3Error) { ErrorLog.Print("%s:%i - Failed to delete folder.\n", _FL); } else if (s == Store3Delayed) { w->Delayed = true; // Leave the work unit on the stack... break; } Pop(w); break; } case RCountSource: { LDataFolderI *f = w->Folder1.Folder; LAssert(f != NULL); if (!f->GetInt(FIELD_IS_ONLINE)) { w->Delayed = true; w->Ts = LCurrentTime(); StatusMsg.Printf("Waiting folder %s", f->GetStr(FIELD_FOLDER_NAME)); UpdateMsg(); RestartOnPulse = true; return true; } Folders++; Pop(w); for (LDataI *i = f->Children().First(); i && !IsCancelled(); i = f->Children().Next()) { int Type = i->Type(); if (Type && Types[Type-MAGIC_BASE]) Items++; } for (LDataFolderI *c = f->SubFolders().First(); c && !IsCancelled(); c = f->SubFolders().Next()) { WorkUnit &wu = Work.New(); wu.Type = RCountSource; wu.Folder1.Folder = c; } break; } case RCountFinish: { // Update the UI Pop(w); OverviewMsg.Printf("%i items in %i folder(s)...", Items, Folders); UpdateMsg(); SetRange(Items); break; } case REndTransaction: { Pop(w); Trans.Reset(); break; } case RCopyFolder: { // Iterate over all the items and push work units onto the stack... LDataFolderI *d = w->Folder2.Dst; LDataFolderI *s = w->Folder2.Src; LAssert(d != NULL && d != NULL); Pop(w); auto Name = s->GetStr(FIELD_FOLDER_NAME); int64 Type = s->GetInt(FIELD_FOLDER_TYPE); StatusMsg.Printf("Copying folder '%s'", Name); UpdateMsg(); CopyStatus *Cs = NULL; if (DeleteSourceOnSuccess) { // Setup a copy status structure.. Cs = new CopyStatus; if (Cs) Status.Add(s, Cs); else LAssert(0); // Create a delete if needed WorkUnit &del = Work.New(); del.Type = RDeleteFolder; del.Folder1.Folder = s; } // Setup an end transaction to commit outstanding data.. Work.New().Type = REndTransaction; if (Type && Types[Type-MAGIC_BASE]) { int ExistsInDestination = 0; switch (Type) { case MAGIC_MAIL: { // Scan existing items for UID's LHashTbl,LDataI*> MsgMap; for (LDataI *e=d->Children().First(); e && !IsCancelled(); e=d->Children().Next()) { auto MsgId = e->GetStr(FIELD_MESSAGE_ID); if (MsgId) MsgMap.Add(MsgId, e); } // Start replicate process for (LDataI *in=s->Children().First(); in && !IsCancelled(); in=s->Children().Next()) { // Check if we have an existing item... auto SrcMsgId = in->GetStr(FIELD_MESSAGE_ID); if (!MsgMap.Find(SrcMsgId)) { // Create a work unit to copy the data... WorkUnit &wu = Work.New(); wu.Type = RCopyData; wu.Data.DstFld = d; wu.Data.SrcFld = s; wu.Data.Src = in; if (Cs) Cs->Total++; } else { ExistsInDestination++; } } break; } case MAGIC_CONTACT: { // Scan for UID's LHashTbl,LDataI*> Uid, Email; const char *c; for (LDataI *e=d->Children().First(); e && !IsCancelled(); e=d->Children().Next()) { if ((c = e->GetStr(FIELD_UID))) Uid.Add(c, e); if ((c = e->GetStr(FIELD_EMAIL))) Email.Add(c, e); } // Start replicate process for (LDataI *in=s->Children().First(); in && !IsCancelled(); in=s->Children().Next()) { // Check if we have an existing item... if (!Uid.Find(in->GetStr(FIELD_UID)) && !Email.Find(in->GetStr(FIELD_EMAIL))) { // Create a work unit to copy the data... WorkUnit &wu = Work.New(); wu.Type = RCopyData; wu.Data.DstFld = d; wu.Data.SrcFld = s; wu.Data.Src = in; if (Cs) Cs->Total++; } else { ExistsInDestination++; } } break; } case MAGIC_FILTER: { // Scan for names LHashTbl,LDataI*> Name; const char *c; for (LDataI *e=d->Children().First(); e && !IsCancelled(); e=d->Children().Next()) { if ((c = e->GetStr(FIELD_FILTER_NAME))) Name.Add(c, e); } // Start replicate process for (LDataI *in=s->Children().First(); in && !IsCancelled(); in=s->Children().Next()) { // Check if we have an existing item... if (!Name.Find(in->GetStr(FIELD_FILTER_NAME))) { // Create a work unit to copy the data... WorkUnit &wu = Work.New(); wu.Type = RCopyData; wu.Data.DstFld = d; wu.Data.SrcFld = s; wu.Data.Src = in; if (Cs) Cs->Total++; } else { ExistsInDestination++; } } break; } } if (ExistsInDestination) { Value(Value() + ExistsInDestination); } } if (Recurse) { // Create a work unit to copy the child folders... // // By doing this at the end things like deletes get done // on child folders first. E.g. for a "move" operation, // which is broken down to copy + delete. WorkUnit &wu = Work.New(); wu.Type = RCopySubFolder; wu.Folder2.Dst = d; wu.Folder2.Src = s; } break; } case RCopySubFolder: { // Iterate over all the items and push work units onto the stack... LDataFolderI *d = w->Folder2.Dst; LDataFolderI *s = w->Folder2.Src; LAssert(d != NULL && d != NULL); Pop(w); LHashTbl,LDataFolderI*> DestMap; for (LDataFolderI *dc = d->SubFolders().First(); dc; dc=d->SubFolders().Next()) { auto DstName = dc->GetStr(FIELD_FOLDER_NAME); if (DstName) DestMap.Add(DstName, dc); } // Replicate sub-folders for (LDataFolderI *sc=s->SubFolders().First(); sc && !IsCancelled(); sc=s->SubFolders().Next()) { auto SrcName = sc->GetStr(FIELD_FOLDER_NAME); if (SrcName) { Store3Status Status = Store3Success; LArray *Tasks = &Work; // Find matching dest folder... LDataFolderI *dc = DestMap.Find(SrcName); if (!dc) { // Not found, so create new destination sub-folder if ((dc = dynamic_cast(d->GetStore()->Create(MAGIC_FOLDER)))) { dc->CopyProps(*sc); Status = dc->Save(d); if (Status == Store3Error) { ErrorLog.Print("%s:%i - Failed to create folder '%s'\n", _FL, SrcName); } else if (Status == Store3Delayed) { WorkUnit &wu = Work.New(); wu.Type = RCreateFolder; wu.Folder2.Dst = d; wu.Folder2.Src = dc; wu.Delayed = true; Tasks = &wu.Deferred; } } } if (dc) { WorkUnit &wu = Tasks->New(); wu.Type = RCopyFolder; wu.Folder2.Dst = dc; wu.Folder2.Src = sc; } } } break; } case RCopyData: { LDataFolderI *d = w->Data.DstFld; LDataI *s = w->Data.Src; LAssert(d != NULL && d != NULL); if (IsCancelled()) { Pop(w); break; } if (!Trans) StartTransaction(d); Store3State State = (Store3State)s->GetInt(FIELD_LOADED); if (State != Store3Loaded) { // Ask for the object to load itself.. s->GetStr(FIELD_TEXT); // Now check again to see what it's doing... State = (Store3State)s->GetInt(FIELD_LOADED); if (State == Store3Loading) { // We should get an OnChange event when it loads... w->Delayed = true; w->Ts = LCurrentTime(); RestartOnPulse = true; return true; } else if (State != Store3Loaded) { // The error case... kill the task FailedWork++; Pop(w); break; } } SaveObject(w); break; } case RReloadFolders: { Pop(w); App->LoadFolders(NULL); break; } case ROpenMailStore: { Store3Status s = Open(w->Open.Store, *w->Open.Spec); if (s == Store3Error) { LString a = w->Open.Spec->Uri.ToString(); ErrorLog.Print("%s:%i - Failed to open data store '%s'\n", _FL, a.Get()); } else if (s == Store3Delayed) { w->Delayed = true; w->Ts = LCurrentTime(); StatusMsg.Printf("Waiting for IMAP connection"); UpdateMsg(); RestartOnPulse = true; return true; } Pop(w); break; } case RCopyStore: { Pop(w); if (SrcStore && DstStore) { LDataFolderI *SrcRoot = SrcStore->GetRoot(); LDataFolderI *DstRoot = DstStore->GetRoot(); if (SrcRoot && DstRoot) { // Setup a job to copy the root sub-folders w = &Work.New(); w->Type = RCopySubFolder; w->Folder2.Dst = DstRoot; w->Folder2.Src = SrcRoot; // Update the UI with the count results... Work.New().Type = RCountFinish; // Setup a copy store task w = &Work.New(); w->Type = RCountSource; w->Folder1.Folder = SrcRoot; } else { ErrorLog.Print("%s:%i - Get roots failed %p/%p\n", _FL, SrcRoot, DstRoot); } } else { ErrorLog.Print("%s:%i - Copy store failed %p/%p\n", _FL, SrcStore.Get(), DstStore.Get()); } break; } default: { LAssert(!"Invalid type."); break; } } uint64 Length = LCurrentTime() - Start; #if DEBUG_LOGGING if (Length > 20) LgiTrace("Work %i took " LPrintfInt64 "\n", w->Type, Length); #endif if (Length >= 50) { // This leaves a air gap for messages to be processed normally. RestartOnPulse = true; return true; } return PostEvent(M_REPLICATE_NEXT); } void UpdateMsg() { LString s, m; if (OverviewMsg) s.Printf("%s\n", OverviewMsg.Get()); s += StatusMsg; if (UnitsTimedOut > 0) { m.Printf(" (%i timed out)", UnitsTimedOut); s += m; } else s += "..."; SetDescription(s); } void OnPulse() { // Check for timeouts... int Prev = UnitsTimedOut; UnitsTimedOut = 0; uint64 Now = LCurrentTime(); for (unsigned i=0; iDelayed) { if (Now - w->Ts > REPLICATE_TIMEOUT) { if (IsCancelled()) { // Kill the work unit... user wants out. Work.DeleteAt(i--); } else { // Show the user there is an issue... UnitsTimedOut++; } } switch (w->Type) { case ROpenMailStore: { if (IsCancelled()) { Work.DeleteAt(i--); } else { // Waiting for IMAP connection... int64 Online = (*w->Open.Store)->GetInt(FIELD_IS_ONLINE); if (Online) { Pop(w); } else { int64 Ms = LCurrentTime() - w->Ts; StatusMsg.Printf("Waiting %.1fsec for IMAP connection", ((double)Ms / 1000.0)); UpdateMsg(); } } break; } case RCountSource: { if (IsCancelled()) Work.DeleteAt(i--); break; } default: break; } } } if (UnitsTimedOut != Prev) UpdateMsg(); if (RestartOnPulse) { RestartOnPulse = false; PostEvent(M_REPLICATE_NEXT); } } bool StartProcess(LDataFolderI *Dst, LDataFolderI *Src, bool recurse, bool deleteSourceOnSuccess, LArray *types) { if (!Dst) { ErrorLog.Print("%s:%i - No destination folder.\n", _FL); return false; } if (!Src) { ErrorLog.Print("%s:%i - No source folder.\n", _FL); return false; } Recurse = recurse; DeleteSourceOnSuccess = deleteSourceOnSuccess; if (types) { ZeroObj(Types); for (unsigned i=0; iLength(); i++) Types[(*types)[i]-MAGIC_BASE] = true; } else { memset(Types, 1, sizeof(Types)); } // Setup a task to copy the folders... WorkUnit *w = &Work.New(); w->Type = RCopyFolder; w->Folder2.Src = Src; w->Folder2.Dst = Dst; // Update the UI with the count results... Work.New().Type = RCountFinish; // Setup a copy store task w = &Work.New(); w->Type = RCountSource; w->Folder1.Folder = Src; // Start the event cycle return PostEvent(M_REPLICATE_NEXT); } bool StartProcess(ReplicateDlg::ReplicateSettings *Settings) { if (!Settings) { ErrorLog.Print("%s:%i - No settings.\n", _FL); return false; } // Set copy type flags ZeroObj(Types); for (unsigned i=0; iTypes.Length(); i++) Types[Settings->Types[i]-MAGIC_BASE] = true; // Setup a task to reload the Scribe folders at the end... WorkUnit *w = &Work.New(); w->Type = RReloadFolders; // Setup a copy store task w = &Work.New(); w->Type = RCopyStore; // Setup load tasks SrcSpec = Settings->Src; DstSpec = Settings->Dst; w = &Work.New(); w->Type = ROpenMailStore; w->Open.Store = &SrcStore; w->Open.Spec = &SrcSpec; w = &Work.New(); w->Type = ROpenMailStore; w->Open.Store = &DstStore; w->Open.Spec = &DstSpec; // Start the event cycle return PostEvent(M_REPLICATE_NEXT); } Store3Status Open(LAutoPtr *Out, ReplicateDlg::AccountSpec &Acc) { if (!Out) return Store3Error; if (Acc.Uri.sProtocol && !_stricmp(Acc.Uri.sProtocol, "imap")) { MailProtocolProgress *prog[2] = { NULL, NULL }; LAutoPtr SettingStore; LDataStoreI *Store = OpenImap( Acc.Uri.sHost, Acc.Uri.Port, Acc.Uri.sUser, Acc.Uri.sPass, Acc.SslFlags, this, 0, prog, 0, 0, SettingStore); if (!Store) return Store3Error; Out->Reset(Store); return Store3Delayed; } else if (Acc.Uri.sProtocol && !_stricmp(Acc.Uri.sProtocol, "file")) { char Path[MAX_PATH_LEN]; if (LIsRelativePath(Acc.Uri.sPath)) { LMakePath(Path, sizeof(Path), App->GetOptions()->GetFile(), ".."); LMakePath(Path, sizeof(Path), Path, Acc.Uri.sPath); } else { strcpy_s(Path, sizeof(Path), Acc.Uri.sPath); } char *Ext = LGetExtension(Path); if (LDirExists(Path)) { if (Ext && !_stricmp(Ext, "mail3")) { LDataStoreI *Store = OpenMail3(Path, this); if (!Store) return Store3Error; Out->Reset(Store); } } else LAssert(!"Unknown store type."); } else LAssert(!"Unknown protocol."); return Store3Success; } ////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////// // LDataEventsI impl ////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////// void Post(LDataStoreI *store, void *Param) { PostEvent(M_STORAGE_EVENT, (LMessage::Param)store, (LMessage::Param)Param); } bool GetSystemPath(int Folder, LVariant &Path) { return App->GetSystemPath(Folder, Path); } LOptionsFile *GetOptions(bool Create = false) { return App->GetOptions(); } void OnNew(LDataFolderI *parent, LArray &items, int pos, bool is_new) { #if DEBUG_LOGGING LgiTrace("%s:%i - OnNew %i\n", _FL, items.Length()); #endif for (ssize_t i=(ssize_t)Work.Length()-1; i>=0; i--) { WorkUnit &w = Work[i]; if (!w.Delayed) continue; switch (w.Type) { case RCreateFolder: { if (items.HasItem( (LDataI*)w.Folder2.Src )) { #if DEBUG_LOGGING LString Str = w.ToString(); LgiTrace("OnNew.%s\n", Str.Get()); #endif Work.DeleteAt(i); } break; } case RCopyData: case RSaveData: { if (items.HasItem( (LDataI*)w.Data.Src )) { CopyStatus *Cs = Status.Find(w.Data.SrcFld); if (Cs) Cs->Ok++; #if DEBUG_LOGGING LString Str = w.ToString(); LgiTrace("OnNew.%s (Cs=%p)\n", Str.Get(), Cs); #endif Work.DeleteAt(i); Value(Value() + 1); } break; } default: break; } } } bool OnDelete(LDataFolderI *parent, LArray &items) { for (ssize_t i=(ssize_t)Work.Length()-1; i>=0; i--) { WorkUnit *w = &Work[i]; if (!w->Delayed) continue; if (w->Type == RDeleteFolder) { if (items.HasItem((LDataI*)w->Folder1.Folder)) { // Finished the delete... #if DEBUG_LOGGING LString Str = w->ToString(); LgiTrace("OnDelete.%s\n", Str.Get()); #endif Pop(w); } } } return true; } bool OnMove(LDataFolderI *new_parent, LDataFolderI *old_parent, LArray &item) { return true; } bool OnChange(LArray &items, int FieldHint) { #if DEBUG_LOGGING LgiTrace("%s:%i - OnChange %i\n", _FL, items.Length()); #endif for (ssize_t i=(ssize_t)Work.Length()-1; i>=0; i--) { WorkUnit &w = Work[i]; if (!w.Delayed) continue; switch (w.Type) { case RCopyData: { // This event fires when the delayed open has completed... if (items.HasItem(w.Data.Src)) { // Finish the save... SaveObject(&w); } break; } default: break; } } return true; } }; struct ReplicateDlgPriv { ScribeWnd *App; LCombo *Src, *Dst; LList *Lst; LArray Paths; LAutoPtr Settings; LAutoString Msg; ReplicateDlgPriv() { App = 0; Src = Dst = 0; Lst = 0; } }; class LType : public LListItem { LListItemCheckBox *Chk; public: int Type; LType(int t, int s) { Type = t; SetText((char*)LLoadString(s), 1); Chk = new LListItemCheckBox(this, 0, true); } bool Value() { return Chk->Value() != 0; } }; ////////////////////////////////////////////////////////////////////////////////////////////////////////// ReplicateDlg::ReplicateDlg(ScribeWnd *app) { d = new ReplicateDlgPriv; SetParent(d->App = app); if (LoadFromResource(IDD_REPLICATE)) { MoveToCenter(); if (GetViewById(IDC_SRC, d->Src) && GetViewById(IDC_DST, d->Dst)) { // Scan for file based mail stores... LXmlTag *Ms = d->App->GetOptions()->LockTag(OPT_MailStores, _FL); if (Ms) { for (auto c: Ms->Children) { char b[256]; sprintf_s(b, sizeof(b), "%s (%s)", c->GetAttr(OPT_MailStoreName), c->GetAttr(OPT_MailStoreLocation)); d->Src->Insert(b); d->Dst->Insert(b); ReplicateDlg::AccountSpec &a = d->Paths.New(); a.Uri.sProtocol = "file"; a.Uri.sPath = c->GetAttr(OPT_MailStoreLocation); } d->App->GetOptions()->Unlock(); } // Scan for IMAP stores... for (auto a : *d->App->GetAccounts()) { LVariant Proto = a->Receive.Protocol(); if (Proto.Str() && !_stricmp(Proto.Str(), PROTOCOL_IMAP4) && !a->Receive.Disabled()) { LVariant Host = a->Receive.Server(); LVariant User = a->Receive.UserName(); LVariant Port = a->Receive.Port(); char b[256]; int Ch = sprintf_s(b, sizeof(b), "imap://%s@%s", User.Str(), Host.Str()); if (Port.CastInt32()) sprintf_s(b+Ch, sizeof(b)-Ch, ":%i", Port.CastInt32()); d->Src->Insert(b); d->Dst->Insert(b); ReplicateDlg::AccountSpec &as = d->Paths.New(); as.SslFlags = MakeOpenFlags(a, false); as.Uri.Empty(); - GPassword Pass; + LPassword Pass; if (a->Receive.GetPassword(&Pass)) { char Ps[256] = ""; Pass.Get(Ps); as.Uri.sPass = Ps; } as.Uri.sProtocol = "imap"; as.Uri.sUser = User.Str(); as.Uri.sHost = Host.Str(); } } } if (GetViewById(IDC_TYPES, d->Lst)) { d->Lst->Insert(new LType(MAGIC_MAIL, IDS_EMAIL)); d->Lst->Insert(new LType(MAGIC_CONTACT, IDS_CONTACT)); d->Lst->Insert(new LType(MAGIC_GROUP, IDC_GROUP)); d->Lst->Insert(new LType(MAGIC_CALENDAR, IDS_CALENDAR)); d->Lst->Insert(new LType(MAGIC_FILTER, IDS_FILTER)); d->Lst->ResizeColumnsToContent(); } OnFolderChange(); } } ReplicateDlg::~ReplicateDlg() { DeleteObj(d); } void ReplicateDlg::OnFolderChange() { SetCtrlEnabled(IDOK, GetCtrlValue(IDC_SRC) != GetCtrlValue(IDC_DST)); } int ReplicateDlg::OnNotify(LViewI *c, LNotification n) { switch (c->GetId()) { case IDC_SRC: case IDC_DST: { OnFolderChange(); break; } case IDOK: { d->Settings.Reset(new ReplicateSettings); List Types; if (d->Lst->GetAll(Types)) { for (auto t: Types) { if (t->Value()) { d->Settings->Types.Add(t->Type); } } } ReplicateDlg::AccountSpec &a = d->Paths[(int32_t)d->Dst->Value()]; d->Settings->Dst = a; a = d->Paths[(uint32_t)d->Src->Value()]; d->Settings->Src = a; // Fall through to EndModal } case IDCANCEL: { EndModal(c->GetId() == IDOK); break; } } return 0; } bool ReplicateDlg::StartProcess() { ScribeReplicator *Rep = new ScribeReplicator(d->App); if (!Rep) return false; return Rep->StartProcess(d->Settings); } Store3Status Store3ReplicateFolders( ScribeWnd *App, LDataFolderI *Dst, LDataFolderI *Src, bool Recurse, bool DeleteSourceOnSuccess, LArray *Types) { if (!Dst || !Src) return Store3Error; ScribeReplicator *Rep = new ScribeReplicator(App); if (!Rep) return Store3Error; if (!Rep->StartProcess(Dst, Src, Recurse, DeleteSourceOnSuccess, Types)) return Store3Error; return Store3Delayed; } diff --git a/Code/Scribe.h b/Code/Scribe.h --- a/Code/Scribe.h +++ b/Code/Scribe.h @@ -1,2569 +1,2570 @@ /*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(); LHostFunc *GetCommands(); LString GetIncludeFile(const char *FileName) override; // 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; + friend class MoveToState; 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); + void MoveTo(LArray &Items, bool CopyOnly, std::function&)> Callback = 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; 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 GetPassword(LPassword *p); + void SetPassword(LPassword *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 LVmCallback *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) { 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; + LPassword 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/ScribeAccountUI.cpp b/Code/ScribeAccountUI.cpp --- a/Code/ScribeAccountUI.cpp +++ b/Code/ScribeAccountUI.cpp @@ -1,345 +1,345 @@ #include "Scribe.h" #include "ScribePrivate.h" #include "ScribeAccountUI.h" #include "resdefs.h" #include "lgi/common/ListItemCheckBox.h" #include "lgi/common/Combo.h" #include "lgi/common/Edit.h" #include "lgi/common/TabView.h" #include "lgi/common/TableLayout.h" #include "lgi/common/Button.h" #include "lgi/common/LgiRes.h" #include "lgi/common/RichTextEdit.h" #include "lgi/common/Charset.h" #define DefaultPortValue LLoadString(IDS_DEFAULT) void PopulateTypes(LCombo *c) { if (c) { c->Insert(PROTOCOL_POP3); c->Insert(PROTOCOL_IMAP4_FETCH); c->Insert(PROTOCOL_IMAP4); c->Insert(PROTOCOL_CALENDAR); c->Insert(PROTOCOL_POP_OVER_HTTP); #ifdef WIN32 c->Insert(PROTOCOL_MAPI); #endif // Old order: // Pop, Imap(fetch), Mapi, Cal, Http } } void FillWithCharsets(LCombo *c, bool All) { if (c) { c->Value(-1); c->Sort(true); c->Sub(GV_STRING); for (LCharset *Cs = LGetCsInfo("us-ascii"); Cs->Charset; Cs++) { if (All || Cs->Type == CpMapped || Cs->Type == CpUtf8) { c->Insert(Cs->Charset); } } } } AccountDlg::AccountDlg(LView *p, ScribeWnd *app, ScribeAccount *a, int Tab) : TabDialog(IDC_TAB, IDC_LAUNCH_HELP) { SetParent(p); App = app; Account = a; Plugins = 0; SendServer = NULL; SendPort = NULL; ReceiveServer = NULL; ReceivePort = NULL; if (LoadFromResource(IDD_ACCOUNT, App->GetUiTags())) { MoveToCenter(); LRichTextEdit *Rte; if (GetViewById(IDC_SIG_HTML, Rte)) Rte->SetStylePrefix("Sig"); PopulateTypes(dynamic_cast(FindControl(IDC_REC_TYPE))); FillWithCharsets(dynamic_cast(FindControl(IDC_SEND_CHARSET1)), false); FillWithCharsets(dynamic_cast(FindControl(IDC_SEND_CHARSET2)), false); FillWithCharsets(dynamic_cast(FindControl(IDC_REC_8BIT_CS)), true); FillWithCharsets(dynamic_cast(FindControl(IDC_REC_ASCII_CP)), true); LCombo *c; if (GetViewById(IDC_SEND_AUTH_TYPE, c)) { c->Insert(LLoadString(IDS_ALL)); c->Insert("PLAIN"); c->Insert("LOGIN"); c->Insert("CRAM-MD5"); c->Insert("OAUTH2"); } if (GetViewById(IDC_RECEIVE_AUTH_TYPE, c)) { c->Insert(LLoadString(IDS_ALL)); c->Insert("PLAIN"); c->Insert("LOGIN"); c->Insert("NTLM"); c->Insert("OAUTH2"); } const char *DefServerTxt = "i.e. mail.isp.com"; if (GetViewById(IDC_SMTP_SERVER, SendServer)) SendServer->SetEmptyText(DefServerTxt); if (GetViewById(IDC_REC_SERVER, ReceiveServer)) ReceiveServer->SetEmptyText(DefServerTxt); GetViewById(IDC_SMTP_PORT, SendPort); GetViewById(IDC_REC_PORT, ReceivePort); Account->SerializeUi(this, true); SetCtrlEnabled(IDC_SMTP_PASSWORD, GetCtrlValue(IDC_SMTP_AUTH) != 0); LNotification note(LNotifyValueChanged); OnNotify(FindControl(IDC_SMTP_SERVER), note); SetCtrlValue(IDC_TAB, Tab); SetCtrlEnabled(IDC_REC_CHECK, GetCtrlValue(IDC_CHECK_EVERY) != 0); LViewI *v = FindControl(IDC_POP3_LEAVE); if (v) OnNotify(v, note); v = FindControl(IDC_REC_TYPE); if (v) OnNotify(v, note); LLayout *t; if (GetViewById(IDC_SIG, t)) { t->SetPourLargest(true); t->Sunken(true); } if (GetViewById(IDC_SIG_HTML, t)) { t->SetPourLargest(true); t->Sunken(true); } } } void AccountDlg::OnCreate() { LTabView *t; if (GetViewById(IDC_SIGNATURE_TAB, t)) { t->SetPourChildren(true); } TabDialog::OnCreate(); } void AccountDlg::UpdateDefaultPort(bool Send) { if (Send) { int DefPort = SMTP_PORT; int64 Ssl = GetCtrlValue(IDC_SEND_SSL); if (Ssl == 2) DefPort = SMTP_SSL_PORT; char s[256]; sprintf_s(s, sizeof(s), "%s: %i", LLoadString(IDC_DEFAULT), DefPort); if (SendPort) { if (SendPort->Value() == 0) SendPort->Name(NULL); SendPort->SetEmptyText(s); } } else { int DefPort = 0; const char *Type = GetCtrlName(IDC_REC_TYPE); int64 Ssl = GetCtrlValue(IDC_RECEIVE_SSL); if (Type) { if (!_stricmp(Type, PROTOCOL_POP3)) { DefPort = (Ssl == 2) ? POP3_SSL_PORT : POP3_PORT; } else if (!_stricmp(Type, PROTOCOL_IMAP4) || !_stricmp(Type, PROTOCOL_IMAP4_FETCH)) { DefPort = (Ssl == 2) ? IMAP_SSL_PORT : IMAP_PORT; } else if (!_stricmp(Type, PROTOCOL_POP_OVER_HTTP)) { DefPort = (Ssl == 2) ? HTTPS_PORT : HTTP_PORT; } } if (DefPort) { char s[256]; sprintf_s(s, sizeof(s), "%s: %i", LLoadString(IDC_DEFAULT), DefPort); if (ReceivePort) { if (ReceivePort->Value() == 0) ReceivePort->Name(NULL); ReceivePort->SetEmptyText(s); } } } } int AccountDlg::OnNotify(LViewI *c, LNotification n) { if (!c) return 0; switch (c->GetId()) { default: { Account->OnNotify(c, n); break; } case IDC_POP3_LEAVE: { bool Leave = c->Value() != 0; SetCtrlEnabled(IDC_DELETE_AFTER, Leave); SetCtrlEnabled(IDC_DELETE_LARGER, Leave); bool Delete = GetCtrlValue(IDC_DELETE_AFTER) != 0; SetCtrlEnabled(IDC_DELETE_DAYS, Leave && Delete); SetCtrlEnabled(IDC_TXT_DAYS, Leave && Delete); Delete = GetCtrlValue(IDC_DELETE_LARGER) != 0; SetCtrlEnabled(IDC_DELETE_SIZE, Leave && Delete); SetCtrlEnabled(IDC_TXT_SIZE, Leave && Delete); break; } case IDC_DELETE_AFTER: { bool Delete = c->Value() != 0; SetCtrlEnabled(IDC_DELETE_DAYS, Delete); SetCtrlEnabled(IDC_TXT_DAYS, Delete); break; } case IDC_DELETE_LARGER: { bool Delete = c->Value() != 0; SetCtrlEnabled(IDC_DELETE_SIZE, Delete); SetCtrlEnabled(IDC_TXT_SIZE, Delete); break; } case IDC_CHECK_EVERY: { SetCtrlEnabled(IDC_REC_CHECK, GetCtrlValue(IDC_CHECK_EVERY) != 0); break; } case IDC_SMTP_SERVER: { bool HasSend = ValidStr(GetCtrlName(IDC_SMTP_SERVER)); SetCtrlEnabled(IDC_ONLY_SEND_THIS, HasSend); if (!HasSend) SetCtrlValue(IDC_ONLY_SEND_THIS, false); break; } case IDC_LAUNCH_HELP: { char Page[256] = "install.html"; switch (GetCtrlValue(IDC_TAB)) { case 0: strcat_s(Page, sizeof(Page), "#acc_id"); break; case 1: strcat_s(Page, sizeof(Page), "#acc_send"); break; case 2: strcat_s(Page, sizeof(Page), "#acc_receive"); break; case 3: strcat_s(Page, sizeof(Page), "#acc_connect"); break; } App->LaunchHelp(Page); break; } case IDC_TAB: { if (c->Value() == 1) { UpdateDefaultPort(true); } else if (c->Value() == 2) { UpdateDefaultPort(false); } break; } case IDC_REC_TYPE: { int64 Type = c->Value(); bool PopType = Type == 0 || Type == 1; SetCtrlEnabled(IDC_POP3_LEAVE, PopType); SetCtrlEnabled(IDC_DELETE_AFTER, PopType); SetCtrlEnabled(IDC_TXT_DAYS, PopType); SetCtrlEnabled(IDC_DELETE_DAYS, PopType); SetCtrlEnabled(IDC_DELETE_LARGER, PopType); SetCtrlEnabled(IDC_DELETE_SIZE, PopType); SetCtrlEnabled(IDC_TXT_MIN, PopType); SetCtrlEnabled(IDC_TXT_KBS, PopType); SetCtrlEnabled(IDC_TXT_DOWNLOAD, PopType); SetCtrlEnabled(IDC_CHECK_EVERY, PopType); SetCtrlEnabled(IDC_REC_CHECK, PopType); SetCtrlEnabled(IDC_MAX_SIZE, PopType); if (!PopType) { SetCtrlValue(IDC_POP3_LEAVE, 0); SetCtrlValue(IDC_DELETE_AFTER, 0); SetCtrlValue(IDC_DELETE_LARGER, 0); } // fall through } case IDC_RECEIVE_SSL: { UpdateDefaultPort(false); break; } case IDC_SEND_SSL: { UpdateDefaultPort(true); break; } case IDOK: { - App->GetAccountSettingsAccess(this, ScribeWriteAccess, [&](auto Allow) + App->GetAccountSettingsAccess(this, ScribeWriteAccess, [this, id=c->GetId()](auto Allow) { if (Allow) { if (!ValidStr(GetCtrlName(IDC_ACCOUNT_NAME))) SetCtrlName(IDC_ACCOUNT_NAME, "My ISP"); Account->SerializeUi(this, false); - EndModal(c->GetId()); + EndModal(id); } }); break; } case IDCANCEL: { EndModal(c->GetId()); break; } } return 0; } diff --git a/Code/ScribeApp.cpp b/Code/ScribeApp.cpp --- a/Code/ScribeApp.cpp +++ b/Code/ScribeApp.cpp @@ -1,12746 +1,12753 @@ /* ** 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); 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; 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 LVmCallback, 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; - + LString CalendarSummary; 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 CallCallback(LVirtualMachine &Vm, LString CallbackName, LScriptArguments &Args) { for (auto s: Scripts) { if (!s->Code) continue; auto Method = s->Code->GetMethod(CallbackName); if (!Method) continue; auto Status = Vm.ExecuteFunction(s->Code, Method, Args); return Status > ScriptError; } Vm.SetDebuggerEnabled(true); // Lets show the UI when we throw the callback not found error. Args.Throw(_FL, "There is no function '%s' for callback.", CallbackName.Get()); return false; } 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; 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) + Dlg->SetButtonCallback(1, [this](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) + LoadFolders([this](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()); } LVmCallback *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); + Calendar::SummaryOfToday(this, [this](auto s) + { + d->CalendarSummary = s; + }); + + return d->CalendarSummary; } 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; + LPassword 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; + LPassword 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) + f->GetMessageById(a, [this, NewFlag=(int)Msg->B()](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) + auto Events = new ScribePrintContext(this, t); + Printer->Print( Events, + [this, Events, Parent, Printer, Callback](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); } + + delete Events; }, 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) + LHtmlMsg([this, WhiteListEmail=LString(WhiteListEmail)](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); + f->MoveTo(Items, false, [this, m](auto result, auto status) + { + 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 ShowDialog = [this]() { auto Dlg = new SecurityDlg(this); Dlg->DoModal(NULL); }; - GPassword p; + LPassword p; if (p.Serialize(GetOptions(), OPT_UserPermPassword, false)) { - GetAccessLevel(this, PermRequireUser, "Security Settings", [&](bool Allow) + GetAccessLevel(this, PermRequireUser, "Security Settings", [ShowDialog](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; 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"))) { LFile f(FileName); if (f) { 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; + LPassword 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; iAddPath(ScribeResourcePath()); Browse->SetEvents(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/ScribeAttachment.cpp b/Code/ScribeAttachment.cpp --- a/Code/ScribeAttachment.cpp +++ b/Code/ScribeAttachment.cpp @@ -1,1383 +1,1384 @@ /* ** FILE: ScribeAttachment.cpp ** AUTHOR: Matthew Allen ** DATE: 7/12/98 ** DESCRIPTION: Scribe Attachments ** ** Copyright (C) 1998, Matthew Allen ** fret@memecode.com */ #include #include #include #include #include "Scribe.h" #include "resdefs.h" #include "lgi/common/NetTools.h" #include "lgi/common/ProgressDlg.h" #include "lgi/common/Tnef.h" #include "lgi/common/LgiRes.h" #include "lgi/common/TextConvert.h" #include "lgi/common/FileSelect.h" extern char *ExtractCodePage(char *ContentType); #ifdef WIN32 char NotAllowed[] = "\\/*:?\"|<>"; #else char NotAllowed[] = "\\/"; #endif ////////////////////////////////////////////////////////////////////////////// char *StripPath(const char *Full) { if (Full) { auto Dos = strrchr(Full, '\\'); auto Unix = strrchr(Full, '/'); if (Dos) { return NewStr(Dos+1); } else if (Unix) { return NewStr(Unix+1); } } return NewStr(Full); } void CleanFileName(char *i) { if (i) { char *o = i; while (*i) { if ((uint8_t)*i >= ' ' && !strchr(NotAllowed, *i)) { *o++ = *i; } i++; } *o++ = 0; } } ////////////////////////////////////////////////////////////////////////////// void Attachment::_New(LDataI *object) { DefaultObject(object); LAssert(GetObject() != NULL); Owner = 0; Msg = 0; IsResizing = false; } Attachment::Attachment(ScribeWnd *App, Attachment *From) : Thing(App) { _New(From && From->GetObject() ? From->GetObject()->GetStore()->Create(MAGIC_ATTACHMENT) : 0); if (From) { char *Ptr = 0; ssize_t Len = 0; if (From->Get(&Ptr, &Len)) { Set(Ptr, Len); SetName(From->GetName()); SetMimeType(From->GetMimeType()); SetContentId(From->GetContentId()); SetCharset(From->GetCharset()); } } } Attachment::Attachment(ScribeWnd *App, LDataI *object, const char *Import) : Thing(App) { _New(object); if (Import) ImportFile(Import); } Attachment::~Attachment() { DeleteObj(Msg); if (Owner) Owner->Attachments.Delete(this); } bool Attachment::ImportFile(const char *FileName) { LAutoPtr f(new LFile); if (!f) { LAssert(!"Out of memory"); return false; } LString Mime = ScribeGetFileMimeType(FileName); if (!f->Open(FileName, O_READ)) { LAssert(!"Can't open file."); return false; } char *c = strrchr((char*)FileName, DIR_CHAR); if (c) SetName(c + 1); else SetName(FileName); if (Mime) SetMimeType(Mime); LAutoStreamI s(f.Release()); GetObject()->SetStream(s); return true; } bool Attachment::ImportStream(const char *FileName, const char *MimeType, LAutoStreamI Stream) { if (!FileName || !MimeType || !Stream) { LAssert(!"Parameter error"); return false; } char *c = strrchr((char*)FileName, DIR_CHAR); if (c) SetName(c + 1); else SetName(FileName); SetMimeType(MimeType); GetObject()->SetStream(Stream); return true; } void Attachment::SetOwner(Mail *msg) { Owner = msg; } bool Attachment::CallMethod(const char *MethodName, LVariant *Ret, LArray &Args) { ScribeDomType Fld = StrToDom(MethodName); *Ret = false; switch (Fld) { case SdSave: // Type: (String FileName) { auto Fn = Args.Length() > 0 ? Args[0]->Str() : NULL; if (Fn) *Ret = SaveTo(Fn, true); return true; } default: break; } return Thing::CallMethod(MethodName, Ret, Args); } bool Attachment::GetVariant(const char *Name, LVariant &Value, const char *Array) { ScribeDomType Fld = StrToDom(Name); switch (Fld) { case SdLength: // Type: Int64 { Value = GetSize(); break; } case SdName: // Type: String { Value = GetName(); break; } case SdMimeType: // Type: String { Value = GetMimeType(); break; } case SdContentId: // Type: String { Value = GetContentId(); break; } case SdData: // Type: Binary { LAutoPtr s(GotoObject(_FL)); if (!s) return false; Value.Empty(); Value.Type = GV_BINARY; if ((Value.Value.Binary.Data = new char[Value.Value.Binary.Length = (int)s->GetSize()])) s->Read(Value.Value.Binary.Data, Value.Value.Binary.Length); break; } case SdType: // Type: Int32 { Value = GetObject()->Type(); break; } default: { return false; } } return true; } Thing &Attachment::operator =(Thing &t) { LAssert(0); return *this; } void IncFileIndex(char *FilePath, size_t FilePathLen) { char File[MAX_PATH_LEN]; char Ext[256] = ""; // Get the extension part char *Dot = strrchr(FilePath, '.'); if (Dot) { strcpy_s(Ext, sizeof(Ext), Dot); } // Get the filename part ssize_t FileLen = (ssize_t)strlen(FilePath) - strlen(Ext); memcpy(File, FilePath, FileLen); File[FileLen] = 0; // Seek to start of digits char *Digits = File + strlen(File) - 1; while (IsDigit(*Digits) && Digits > File) { Digits--; } if (!IsDigit(*Digits)) Digits++; // Increment the index int Index = atoi(Digits); sprintf_s(Digits, sizeof(File)-(Digits-File), "%i", Index + 1); // Write the resulting filename sprintf_s(FilePath, FilePathLen, "%s%s", File, Ext); } void Attachment::SetMsg(Mail *m) { Msg = m; } Mail *Attachment::GetMsg() { if (!Msg && IsMailMessage()) { // is an email LAutoStreamI f = GetObject()->GetStream(_FL); if (f) { if ((Msg = new Mail(App))) { Msg->SetWillDirty(false); Msg->App = App; Msg->ParentFile = this; Msg->OnAfterReceive(f); } } } return Msg; } bool Attachment::GetIsResizing() { return IsResizing; } void Attachment::SetIsResizing(bool b) { IsResizing = b; Update(); } bool Attachment::IsMailMessage() { return GetMimeType() && !_stricmp(GetMimeType(), sMimeMessage); } bool Attachment::IsVCalendar() { return GetMimeType() && ( !_stricmp(GetMimeType(), sMimeVCalendar) || !_stricmp(GetMimeType(), sMimeICalendar) ); } bool Attachment::IsVCard() { return GetMimeType() && !_stricmp(GetMimeType(), sMimeVCard); } char *Attachment::GetDropFileName() { if (!DropFileName) DropFileName = MakeFileName(); return DropFileName; } bool Attachment::GetDropFiles(LString::Array &Files) { bool Status = false; if (GetDropFileName()) { char p[MAX_PATH_LEN]; LMakePath(p, sizeof(p), ScribeTempPath(), DropFileName); if (SaveTo(p, true)) { Files.Add(p); Status = true; } } return Status; } LAutoString Attachment::MakeFileName() { auto Name = GetName(); LAutoString CleanName; if (Name) { CleanName.Reset(StripPath(Name)); CleanFileName(CleanName); } else { LArray Ext; char s[256] = "Attachment"; auto MimeType = GetMimeType(); LGetMimeTypeExtensions(MimeType, Ext); if (Ext.Length()) { size_t len = strlen(s); sprintf_s(s+len, sizeof(s)-len, ".%s", Ext[0].Get()); } CleanName.Reset(NewStr(s)); } return CleanName; } void Attachment::OnOpen(LView *Parent, char *Dest) { bool VCal; if (GetMsg()) { // Open the mail message... Msg->DoUI(Owner); } else if ((VCal = IsVCalendar()) || IsVCard()) { // Open the event or contact... Thing *c = App->CreateItem(VCal ? MAGIC_CALENDAR : MAGIC_CONTACT, 0, false); if (c) { LAutoPtr f(GotoObject(_FL)); if (f) { if (c->Import(f, GetMimeType())) { c->DoUI(); } else LgiMsg(Parent, "Failed to parse calendar.", "Error"); } else LgiTrace("%s:%i - Failed to get attachment stream.\n", _FL); } } else // is some generic file { bool IsExe = false; int TnefSizeLimit = 8 << 20; LStream *TnefStream = 0; LArray TnefIndex; int64 AttachPos = -1; LAutoStreamI f = GetObject()->GetStream(_FL); if (f) { IsExe = LIsFileExecutable(GetName(), f, AttachPos = f->GetPos(), f->GetSize()); if (!IsExe && f->GetSize() < TnefSizeLimit) { f->SetPos(AttachPos); if (!TnefReadIndex(f, TnefIndex)) { DeleteObj(TnefStream); } } f.Reset(); } if (IsExe) { LgiMsg(Parent, LLoadString(IDS_ERROR_EXE_FILE), AppName); return; } // Check for TNEF if (TnefStream) { LStringPipe p; for (unsigned n=0; nSize); p.Print("\t%s (%s)\n", TnefIndex[n]->Name, Size); } char *FileList = p.NewStr(); if (Owner && LgiMsg(Parent, LLoadString(IDS_ASK_TNEF_DECODE), AppName, MB_YESNO, FileList) == IDYES) { char *Tmp = ScribeTempPath(); if (Tmp) { for (unsigned i=0; iName); LFile Out; if (Out.Open(s, O_WRITE)) { Out.SetSize(0); if (TnefExtract(TnefStream, &Out, TnefIndex[i])) { Out.Close(); Attachment *NewFile = 0; Owner->AttachFile(NewFile = new Attachment(App, GetObject()->GetStore()->Create(MAGIC_ATTACHMENT), s)); if (NewFile && Owner->GetUI()) { AttachmentList *Lst = Owner->GetUI()->GetAttachments(); if (Lst) { Lst->Insert(NewFile); Lst->ResizeColumnsToContent(); } } } Out.Close(); } } } Owner->Save(); OnDeleteAttachment(Parent, false); } DeleteObj(TnefStream); DeleteArray(FileList); } else { // Open file... LAutoString FileToExecute; char *Tmp = ScribeTempPath(); if (!Tmp) return; // get the file name char FileName[MAX_PATH_LEN]; LAutoString CleanName = MakeFileName(); if (CleanName) { LMakePath(FileName, sizeof(FileName), Tmp, CleanName); while (LFileExists(FileName)) { IncFileIndex(FileName, sizeof(FileName)); } // write the file out if (SaveTo(FileName)) { FileToExecute.Reset(NewStr(FileName)); } } // open the file auto Mime = GetMimeType(); if (FileToExecute) { LAutoString AssociatedApp; LXmlTag *FileTypes = App->GetOptions()->LockTag(OPT_FileTypes, _FL); if (FileTypes) { auto Mime = GetMimeType(); auto FileName = GetName(); for (auto t: FileTypes->Children) { char *mt = t->GetAttr("mime"); char *ext = t->GetAttr("extension"); bool MimeMatch = mt && Mime && !_stricmp(mt, Mime); bool ExtMatch = ext && FileName && MatchStr(ext, FileName); if (MimeMatch || ExtMatch) { AssociatedApp.Reset(TrimStr(t->GetContent())); break; } } App->GetOptions()->Unlock(); } if (AssociatedApp) { const char *s = AssociatedApp; LAutoString Exe(LTokStr(s)); if (Exe) { char Args[MAX_PATH_LEN+100]; if (!strchr(s, '%') || sprintf_s(Args, sizeof(Args), s, FileToExecute.Get()) < 0) { if (sprintf_s(Args, sizeof(Args), "\"%s\"", FileToExecute.Get()) < 0) Args[0] = 0; } if (Args[0] && LExecute(Exe, Args)) { // Successful.. return; } } } if (!LExecute(FileToExecute, 0, Tmp)) { // if the default open fails.. open as text LString AppPath = LGetAppForMimeType(Mime ? Mime : sTextPlain); bool Status = false; if (AppPath) { char *s = strchr(AppPath, '%'); if (s) s[0] = 0; Status = LExecute(AppPath, FileToExecute, Tmp); } if (!Status) { LgiMsg(Parent, "Couldn't open file.", AppName, MB_OK); } } } } } } void Attachment::OnDeleteAttachment(LView *Parent, bool Ask) { if (Owner) { LVariant ConfirmDelete; App->GetOptions()->GetValue(OPT_ConfirmDelete, ConfirmDelete); if (!Ask || !ConfirmDelete.CastInt32() || LgiMsg(GetList(), LLoadString(IDS_DELETE_ASK), AppName, MB_YESNO) == IDYES) { List Sel; LList *p = GetList(); if (p) { p->GetSelection(Sel); } else { Sel.Insert(this); } for (LListItem *i: Sel) { Attachment *a = dynamic_cast(i); if (a) { if (p) { p->Remove(i); } a->Owner->DeleteAttachment(a); } } if (p) { p->Invalidate(); } } } } bool Attachment::SaveTo(char *FileName, bool Quite, LView *Parent) { bool Status = false; if (FileName) { LFile Out; Status = true; if (LFileExists(FileName)) { if (Quite) { return true; } else { Out.Close(); LString Msg = AskOverwriteMsg(FileName); Status = LgiMsg(Parent ? Parent : App, Msg, AppName, MB_YESNO) == IDYES; } } if (!Out.Open(FileName, O_WRITE)) { if (!Quite) LgiMsg(App, LLoadString(IDS_ERROR_CANT_WRITE), AppName, MB_OK, FileName); else LgiTrace("%s:%i - Can't open '%s' for writing (err=0x%x)\n", _FL, FileName, Out.GetError()); Status = false; } if (Status) { Out.SetSize(0); if (GetObject()) { int BufSize = 64 << 10; char *Buf = new char[BufSize]; LStreamI *f = GotoObject(_FL); if (f && Buf) { f->SetPos(0); Out.SetSize(0); int64 MySize = f->GetSize(); int64 s = MySize; while (s > 0) { ssize_t r = (int)MIN(BufSize, s); r = f->Read(Buf, r); if (r > 0) { Out.Write(Buf, r); s -= r; } else break; } int64 OutPos = Out.GetPos(); if (OutPos < MySize) { // Error writing to disk... Out.Close(); FileDev->Delete(FileName, false); LAssert(!"Failed to write whole attachment to disk."); Status = false; } } else { LgiTrace("%s:%i - GotoObject failed.\n", _FL); Status = false; } DeleteObj(f); DeleteArray(Buf); } /* else if (Data) { int Written = Out.Write(Data, Size); Status = Written == Size; } */ } } return Status; } void Attachment::OnSaveAs(LView *Parent) { auto Name = GetName(); char *n = StripPath(Name); if (!n) { n = NewStr("untitled"); } if (n) { CleanFileName(n); auto Select = new LFileSelect(Parent); Select->Type("All files", LGI_ALL_FILES); Select->Name(n); List Files; if (LListItem::Parent) { LListItem::Parent->GetSelection(Files); } else { Files.Insert(this); } if (Files.Length() > 0) { - auto DoSave = [&](LFileSelect *Select) + auto DoSave = [this, Files, Parent](LFileSelect *Select) { char Dir[MAX_PATH_LEN]; strcpy_s(Dir, sizeof(Dir), Select->Name()); if (Files.Length() > 1) { // Loop through all the files and write them to that directory - for (LListItem *i: Files) + for (unsigned idx=0; idx(i); if (a) { char Path[MAX_PATH_LEN]; auto d = StripPath(a->GetName()); if (d) { sprintf_s(Path, sizeof(Path), "%s%s%s", Dir, DIR_STR, d); a->SaveTo(Path); DeleteArray(d); } } } } else { // Write the file Attachment *a = dynamic_cast(Files[0]); if (a) { a->SaveTo(Dir, false, Parent); } } }; if (Files.Length() > 1) { // multiple files, ask which directory to write to - Select->OpenFolder([&](auto dlg, auto status) + Select->OpenFolder([DoSave](auto dlg, auto status) { if (status) DoSave(dlg); delete dlg; }); } else { // single file, ask for filename and path - Select->Save([&](auto dlg, auto status) + Select->Save([DoSave](auto dlg, auto status) { if (status) DoSave(dlg); delete dlg; }); } } DeleteArray(n); } } bool Attachment::OnKey(LKey &k) { if (k.vkey == LK_RETURN && k.IsChar) { if (k.Down()) { OnOpen(GetList()); } return true; } return false; } void Attachment::OnMouseClick(LMouse &m) { auto mt = GetMimeType(); bool OpenAttachment = false; if (m.IsContextMenu()) { // open the right click menu LSubMenu RClick; LString MimeType = GetMimeType(); RClick.AppendItem(LLoadString(IDS_ADD_TO_CAL), IDM_ADD_TO_CAL, IsVCalendar()); RClick.AppendSeparator(); RClick.AppendItem(LLoadString(IDS_OPEN), IDM_OPEN, true); RClick.AppendItem(LLoadString(IDS_SAVEAS), IDM_SAVEAS, true); RClick.AppendItem(LLoadString(IDS_DELETE), IDM_DELETE, true); RClick.AppendSeparator(); RClick.AppendItem(LLoadString(IDS_RESIZE), IDM_RESIZE, MimeType.Lower().Find("image/") >= 0); if (Parent->GetMouse(m, true)) { switch (RClick.Float(Parent, m.x, m.y)) { case IDM_OPEN: { OpenAttachment = true; break; } case IDM_DELETE: { OnDeleteAttachment(Parent, true); break; } case IDM_SAVEAS: { OnSaveAs(Parent); break; } case IDM_ADD_TO_CAL: { ScribeFolder *Cal = App->GetFolder(FOLDER_CALENDAR); if (!Cal) LgiMsg(Parent, "Can't find the calendar folder.", AppName); else { Thing *c = App->CreateItem(MAGIC_CALENDAR, 0, false); if (c) { LAutoPtr f(GotoObject(_FL)); if (f) { if (c->Import(c->AutoCast(f), mt)) { c->Save(Cal); c->DoUI(); } else LgiTrace("%s:%i - Failed to import cal stream.\n", _FL); } else LgiTrace("%s:%i - Failed to get attachment stream.\n", _FL); } else LgiTrace("%s:%i - Failed to create calendar obj.\n", _FL); } break; } case IDM_RESIZE: { List Sel; if (GetList() && GetList()->GetSelection(Sel)) { for (auto a: Sel) { if (!a->Owner) { LgiTrace("%s:%i - No owner?", _FL); break; } a->Owner->ResizeImage(a); } GetList()->ResizeColumnsToContent(); } break; } } } } else if (m.Double()) { OpenAttachment = true; } if (OpenAttachment) { // open the attachment OnOpen(Parent); } } bool Attachment::GetFormats(LDragFormats &Formats) { Formats.SupportsFileDrops(); return Formats.Length() > 0; } bool Attachment::GetData(LArray &Data) { int SetCount = 0; for (unsigned idx=0; idx Att; if (Parent->GetSelection(Att)) { LString::Array Files; for (auto a: Att) { // char *Nm = a->GetName(); if (!a->DropSourceFile || !LFileExists(a->DropSourceFile)) { a->DropSourceFile.Reset(); char p[MAX_PATH_LEN]; LAutoString Clean = a->MakeFileName(); LMakePath(p, sizeof(p), ScribeTempPath(), Clean); char Ext[256]; char *d = strrchr(p, '.'); if (!d) d = p + strlen(p); strcpy_s(Ext, sizeof(Ext), d); for (int i=1; LFileExists(p); i++) { sprintf_s(d, sizeof(p)-(d-p), "_%i%s", i, Ext); } if (a->SaveTo(p, true)) { a->DropSourceFile.Reset(NewStr(p)); } } if (a->DropSourceFile) { Files.Add(a->DropSourceFile.Get()); } else LAssert(0); } if (Files.First()) { LMouse m; App->GetMouse(m, true); if (CreateFileDrop(&dd, m, Files)) { SetCount++; } } } } } return SetCount > 0; } LStreamI *Attachment::GotoObject(const char *file, int line) { if (!GetObject()) return 0; LAutoStreamI s = GetObject()->GetStream(file, line); return s.Release(); } int Attachment::Sizeof() { return 0; } bool Attachment::Serialize(LFile &f, bool Write) { /* ulong Magic = MAGIC_ATTACHMENT; LView *Parent = Window; if (Owner && Owner->GetUi()) { Parent = Owner->GetUi(); } if (Write) { f << Magic; f << Content; // Check we have the data to write out, as it will effect the // size we write out before the data bool DataOk = false; LFile In; if (Data) { DataOk = true; } else if (ImportName) { // Check we can open the file... while (!In.Open(ImportName, O_READ)) { char Msg[256]; sprintf_s(Msg, sizeof(Msg), LLoadString(IDS_ERROR_CANT_READ), ImportName); LAlert Dlg( Parent, AppName, Msg, LLoadString(IDS_RETRY), LLoadString(IDS_CANCEL)); int Result = Dlg.DoModal(); if (Result == 2) { break; } } DataOk = In.IsOpen(); } else { // We're skipping the data already on disk DataOk = true; } if (!DataOk) { // No point continuing return false; } f << Size; WriteStr(f, Name); if (Data) { // write the file itself f.Write(Data, Size); } else if (ImportName) { // import from the file uint64 Last = LCurrentTime(); LProgressDlg *Prog = 0; int BufSize = 64 << 10; uchar *Buf = new uchar[BufSize]; if (Buf) { int s = Size; while (s > 0 && f.GetStatus()) { int r = min(s, BufSize); r = In.Read(Buf, r); f.Write(Buf, r); s -= r; uint64 Now = LCurrentTime(); if (Prog) { if (Now - Last > 300) { Prog->Value((Size-s) >> 10); LYield(); Last = Now; if (Prog->Cancel()) { DeleteArray(Buf); DeleteObj(Prog); return false; } } } else if (Now - Last > 1000) { Prog = new LProgressDlg(Parent); if (Prog) { Prog->SetDescription("Importing file..."); Prog->SetLimits(0, Size >> 10); Prog->SetType("K"); } Last = Now; } } DeleteArray(ImportName); DeleteObj(Prog); } DeleteArray(Buf); } else { // skip over data on hard disk f.Seek(Size, SEEK_CUR); } // new style fields if (MimeType) { WriteStrField(FIELD_MIME_TYPE, MimeType); } if (ContentId) { WriteStrField(FIELD_CONTENT_ID, ContentId); } } else { f >> Magic; if (Magic == MAGIC_ATTACHMENT) // The versions before v1.25 didn't // set this correctly, but that is so old // now, I've removed the hack to allow // the attachment in. { f >> Content; f >> Size; DeleteArray(Name); Name = ReadStr(f PassDebugArgs); // Skip over the data DeleteArray(Data); int StartPos = f.GetPos(); f.Seek(Size, SEEK_CUR); int ExtraPos = f.GetPos(); // read list of new-style fields bool Done = false; bool Eob = false; while ( !(Eob = Store->EndOfObj(f)) && !Done) { short FieldId = 0; ulong FieldSize = 0; f >> FieldId; switch (FieldId) { ReadStrField(FIELD_MIME_TYPE, MimeType); ReadStrField(FIELD_CONTENT_ID, ContentId); default: { // Error: unknown chunk SetDirty(); Done = true; break; } } } LFormatSize(SizeStr, Size); } else return false; } return f.GetStatus(); */ return false; } char *GetSubField(char *s, char *Field, bool AllowConversion = true) { char *Status = 0; if (s && Field) { s = strchr(s, ';'); if (s) { s++; size_t FieldLen = strlen(Field); char White[] = " \t\r\n"; while (*s) { // Skip leading whitespace while (*s && (strchr(White, *s) || *s == ';')) s++; // Parse field name if (IsAlpha(*s)) { char *f = s; while (*s && (IsAlpha(*s) || *s == '-')) s++; bool HasField = ((s-f) == FieldLen) && (_strnicmp(Field, f, FieldLen) == 0); while (*s && strchr(White, *s)) s++; if (*s == '=') { s++; while (*s && strchr(White, *s)) s++; if (*s && strchr("\'\"", *s)) { // Quote Delimited Field char d = *s++; char *e = strchr(s, d); if (e) { if (HasField) { if (AllowConversion) { Status = DecodeRfc2047(NewStr(s, e-s)); } else { Status = NewStr(s, e-s); } break; } s = e + 1; } else break; } else { // Delimited Field char *e = s; while (*e && *e != ';') e++; if (HasField) { Status = DecodeRfc2047(NewStr(s, e-s)); break; } s = e; } } else break; } else break; } } } return Status; } const char *Attachment::GetText(int i) { if (FieldArray.Length()) { return "This is an attachment!!!!!"; } else { switch (i) { case 0: { auto Nm = GetName(); if (IsResizing) { Buf.Printf("%s (%s)", Nm, LLoadString(IDS_RESIZING)); return Buf; } return Nm; } case 1: { static char s[64]; LFormatSize(s, sizeof(s), GetSize()); return s; } case 2: { return GetMimeType(); } case 3: { return GetContentId(); } } } return 0; } bool Attachment::Get(char **ptr, ssize_t *size) { if (!ptr || !size) return false; LStreamI *f = GotoObject(_FL); if (!f) return false; *size = f->GetSize(); *ptr = new char[*size+1]; if (*ptr) { auto r = f->Read(*ptr, *size); (*ptr)[r] = 0; } DeleteObj(f); return true; } bool Attachment::Set(LAutoStreamI Stream) { if (!GetObject()) { LAssert(0); return false; } if (!GetObject()->SetStream(Stream)) { LAssert(0); return false; } return true; } bool Attachment::Set(char *ptr, ssize_t size) { LAutoStreamI s(new LMemStream(ptr, size)); return Set(s); } diff --git a/Code/ScribeFilter.cpp b/Code/ScribeFilter.cpp --- a/Code/ScribeFilter.cpp +++ b/Code/ScribeFilter.cpp @@ -1,4212 +1,4214 @@ /* ** 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: 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 = 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: 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 = 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) + s->OpenFolder([this](auto s, auto status) { if (status) SetCtrlName(IDC_DIR, s->Name()); - delete dlg; + delete s; }); 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 = 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 = t->GetAttr(OPT_Arg1); return true; } LDataPropI &FilterAction::operator =(LDataPropI &p) { FilterAction *c = dynamic_cast(&p); if (c) { Type = c->Type; 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, LString Pattern, List &Files) { Files.Empty(); if (m) { List Attachments; if (m->GetAttachments(&Attachments)) { 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); + Folder->MoveTo(Items, false, [this, Log](auto result, auto status) + { + if (Log) + Log->Print("\tACTION_MOVE_TO_FOLDER(%s) = %i.\n", Arg1.Get(), result); + }); + Status = true; } 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); + Folder->MoveTo(Items, true, [this, Log](auto result, auto status) + { + if (Log) + Log->Print("\tACTION_COPY(%s) = %i.\n", Arg1.Get(), result); + }); + Status = true; } 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 = 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.Get(), "false") == 0) { // unmark the item... m->SetMarkColour(0); } else { // parse out RGB 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 = Dlg->Get(); delete dlg; }); break; } case ACTION_EXPORT: { auto s = new LFileSelect(Parent); s->OpenFolder([this](auto s, auto status) { if (status) 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 = "local"; break; } case IDM_SERVER: { Arg1 = "server"; break; } case IDM_LOCAL_AND_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 = "true"; break; } case IDM_FALSE: { Arg1 = "false"; break; } case IDM_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 = "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 = 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 s, auto ok) { 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 = 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 = 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 = 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 = 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 = 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 = 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/ScribeFolder.cpp b/Code/ScribeFolder.cpp --- a/Code/ScribeFolder.cpp +++ b/Code/ScribeFolder.cpp @@ -1,4486 +1,4564 @@ /* ** FILE: ScribeFolder.cpp ** AUTHOR: Matthew Allen ** DATE: 17/1/2000 ** DESCRIPTION: Scribe folder's ** ** Copyright (C) 2000-2002, Matthew Allen ** fret@memecode.com */ // Includes #include "Scribe.h" #include "lgi/common/DropFiles.h" #include "lgi/common/ProgressDlg.h" #include "lgi/common/LgiQuickSort.h" #include "lgi/common/DisplayString.h" #include "lgi/common/TextFile.h" #include "lgi/common/LgiRes.h" #include "lgi/common/FileSelect.h" #include "lgi/common/ClipBoard.h" #include "Calendar.h" #include "resdefs.h" #include "ReplicateDlg.h" #include "FolderTask.h" ////////////////////////////////////////////////////////////////////////////// #if WINNATIVE #include "lgi/common/Com.h" #endif class LDndFilePromise #if defined(WINDOWS) #elif defined(__GTK_H__) #elif defined(MAC) #endif { LStream *Src; #if WINNATIVE FILEGROUPDESCRIPTOR Gd; #elif defined(__GTK_H__) #elif defined(MAC) #endif public: LDndFilePromise(LDragData &dd, LStream *src, LString FileName) { Src = src; #if WINNATIVE ZeroObj(Gd); Gd.cItems = 1; // CLSID Fd.clsid; // SIZEL Fd.sizel; // POINTL Fd.pointl; // FILETIME Fd.ftCreationTime; // FILETIME Fd.ftLastAccessTime; // FILETIME Fd.ftLastWriteTime; FILEDESCRIPTOR &Fd = Gd.fgd[0]; Fd.dwFlags = FD_ATTRIBUTES | FD_PROGRESSUI; Fd.dwFileAttributes = FILE_ATTRIBUTE_COMPRESSED | FILE_ATTRIBUTE_NORMAL; int64 Sz = Src->GetSize(); if (Sz >= 0) { Fd.dwFlags |= FD_FILESIZE; Fd.nFileSizeHigh = Sz >> 32; Fd.nFileSizeLow = Sz & 0xffffffff; } auto Leaf = FileName.RFind(DIR_STR); LAutoWString FnW(Utf8ToWide(Leaf >= 0 ? FileName(Leaf+1,-1) : FileName)); Strcpy(Fd.cFileName, CountOf(Fd.cFileName), FnW.Get()); LVariant &v = dd.Data[0]; v.SetBinary(sizeof(Gd), &Gd); #elif defined(__GTK_H__) LAssert(!"Not impl."); #elif LGI_COCOA LAssert(!"Not impl."); #elif LGI_CARBON // See Apple: Technical Note TN1085 // https://web.archive.org/web/20080725134839/http://developer.apple.com/technotes/tn/tn1085.html // flavorTypePromiseHFS = 'phfs' PromiseHFSFlavor Promise; ZeroObj(Promise); Promise.fileType = 'mbox'; Promise.fileCreator = '****'; Promise.promisedFlavor = kDragPromisedFlavor; // 'fssP' int Sz = sizeof(Promise); LVariant &v1 = dd.Data[0]; v1.SetBinary(sizeof(Promise), &Promise, false); // The Dnd code will add the 2nd part of the promise // There isn't the visibility into that API at this level #else #error "Impl me." #endif } ~LDndFilePromise() { #if defined(WINDOWS) #elif defined(__GTK_H__) #elif defined(MAC) #endif } #if defined(WINDOWS) #elif defined(__GTK_H__) #elif defined(MAC) #endif int AddContents(LDragData &dd) { dd.Data[0] = Src; return 1; } }; ////////////////////////////////////////////////////////////////////////////// #define PROFILE_POPULATE 0 #define PROFILE_LOAD_THINGS 0 int FoldersCurrentlyLoading = 0; char ScribeFolderObject[] = "com.memecode.Folder"; class ScribeFolderPriv { public: int8 IsInbox = -1; bool InUpdateUnread = false; LAutoPtr DsBase; LAutoPtr DsUnread; LAutoPtr FilePromise; }; ////////////////////////////////////////////////////////////////////////////// ScribeFolder::ScribeFolder() { d = new ScribeFolderPriv; } ScribeFolder::~ScribeFolder() { bool IsRoot = !GetParent(); if (CurState != FldState_Idle) { // int Cur = FoldersCurrentlyLoading; #ifdef _DEBUG LAssert(!"Can't delete folder while it's busy."); #else LgiMsg( App, "Can't unload folder while busy. Please report what\n" "you were attempting to do to fret@memecode.com", "Scribe Error", MB_OK); #endif return; } switch (GetItemType()) { case MAGIC_CALENDAR: CalendarSource::FolderDelete(this); // fall through case MAGIC_CONTACT: case MAGIC_FILTER: case MAGIC_GROUP: App->RemoveThingSrc(this); break; default: break; } if (View()) { bool IsMe = View()->GetContainer() == this; // bool IsSelect = Select(); View()->DeletePlaceHolders(); if (IsMe) { View()->SetContainer(0); View()->RemoveAll(); } } Thing *t; int i = 0; while ((t = Items[i])) { if (!t->DecRef()) i++; } Update(); EmptyFieldList(); DeleteObj(d); ScribeFolder *f = GetChildFolder(); DeleteObj(f); if (!IsRoot) { f = GetNextFolder(); DeleteObj(f); } // Don't delete 'Object' here, it's owned by the backend storage object } bool ScribeFolder::SetObject(LDataI *o, bool InDestructor, const char *File, int Line) { if (CurState != FldState_Idle) { LAssert(!"Can't set object while folder is not idle."); return false; } return LDataUserI::SetObject(o, InDestructor, File, Line); } void ScribeFolder::UpdateOsUnread() { #ifdef WIN32 if (App->GetFolder(FOLDER_INBOX) == this) { LHashTbl, int> Un(0, -1); // Pre-populate 'Un' with our email accounts List *Acc = App->GetAccounts(); if (Acc) { for (auto a: *Acc) { LVariant e = a->Identity.Email(); if (e.Str()) Un.Add(e.Str(), 0); } } // Scan folder for unread email... for (auto t : Items) { Mail *m = t->IsMail(); if (m) { if (!TestFlag(m->GetFlags(), MAIL_READ)) { ScribeAccount *a = m->GetAccountSentTo(); if (a) { LVariant Email; Email = a->Identity.Email(); if (Email.Str()) { int Cur = Un.Find(Email.Str()); Un.Add(Email.Str(), Cur + 1); } } } } } #if 0 // Set system email status LArray Ver; int Os = LGetOs(&Ver); if ((Os == LGI_OS_WIN32 || Os == LGI_OS_WIN64) && (Ver[0] > 5 || (Ver[0] == 5 && Ver[1] > 0))) { char e[256]; LgiGetExeFile(e, sizeof(e)); typedef HRESULT (__stdcall *pSHSetUnreadMailCount)(LPCWSTR pszMailAddress, DWORD dwCount,LPCWSTR pszShellExecuteCommand); LLibrary Shell32("shell32"); pSHSetUnreadMailCount SHSetUnreadMailCount = (pSHSetUnreadMailCount) Shell32.GetAddress("SHSetUnreadMailCountW"); if (SHSetUnreadMailCount) { LVariant Exe; Exe = e; char *k; for (int u=Un.First(&k); u>=0; u=Un.Next(&k)) { LVariant Email = k; SHSetUnreadMailCount(Email.WStr(), u, Exe.WStr()); } } } #endif } #endif } void ScribeFolder::SetLoadOnDemand() { if ((LoadOnDemand = new LTreeItem)) { Expanded(false); LoadOnDemand->SetText((char*)LLoadString(IDS_LOADING)); Insert(LoadOnDemand); } } void ScribeFolder::GetMessageById(const char *Id, std::function Callback) { if (!Id) { if (Callback) Callback(NULL); return; } - LoadThings(NULL, [&](auto s) + LoadThings(NULL, [this, Id=LString(Id), Callback](auto s) { if (s < Store3Delayed) { if (Callback) Callback(NULL); return; } for (auto t : Items) { Mail *r = t->IsMail(); if (!r) continue; auto rid = r->GetMessageId(); - if (!Stricmp(rid, Id)) + if (!Stricmp(rid, Id.Get())) { if (Callback) Callback(r); return; } } }); } bool ScribeFolder::InsertThing(Thing *t) { bool Status = false; if (t && Select() && View()) { // Filter ThingFilter *Filter = App->GetThingFilter(); t->SetFieldArray(FieldArray); if (!Filter || Filter->TestThing(t)) { if (t->GetList() != View()) { View()->Insert(t); #if WINNATIVE UpdateWindow(View()->Handle()); #endif } } Status = true; } return Status; } bool ScribeFolder::GetThreaded() { return GetObject() ? GetObject()->GetInt(FIELD_FOLDER_THREAD) != 0 : false; } void ScribeFolder::SetThreaded(bool t) { if (GetObject() && GetThreaded() ^ t) { GetObject()->SetInt(FIELD_FOLDER_THREAD, t); SetDirty(); } } ThingList *ScribeFolder::View() { return App ? App->GetMailList() : 0; } bool ScribeFolder::HasFieldId(int Id) { auto o = GetFldObj(); if (o) { for (LDataPropI *f = o->Fields().First(); f; f = o->Fields().Next()) { if (Id == f->GetInt(FIELD_ID)) { return true; } } } return false; } void ScribeFolder::SetFolderPerms(LView *Parent, ScribeAccessType Access, ScribePerm Perm, std::function Callback) { ScribePerm Current = GetFolderPerms(Access); int Field = (Access == ScribeReadAccess) ? FIELD_FOLDER_PERM_READ : FIELD_FOLDER_PERM_WRITE; if (GetObject() && Current != Perm) { - App->GetAccessLevel(Parent, Current, GetPath(), [&](auto Allow) + App->GetAccessLevel(Parent, Current, GetPath(), [this, Field, Perm, Callback](auto Allow) { if (Allow) { GetObject()->SetInt(Field, Perm); SetDirty(); } if (Callback) Callback(Allow); }); } else { if (Callback) Callback(true); // i.e. not changing } } ScribePerm ScribeFolder::GetFolderPerms(ScribeAccessType Access) { int Field = (Access == ScribeReadAccess) ? FIELD_FOLDER_PERM_READ : FIELD_FOLDER_PERM_WRITE; return GetObject() ? (ScribePerm)GetObject()->GetInt(Field) : PermRequireNone; } Store3Status ScribeFolder::CopyTo(ScribeFolder *NewParent, int NewIndex) { Store3Status Copied = Store3Error; if (NewParent == GetParent()) { NewParent->GetObject()->GetStore(); } else if (GetObject() && GetObject()->GetStore() && NewParent->GetObject() && NewParent->GetObject()->GetStore()) { // Find or create the destination folder LDataFolderI *Dst = 0; auto Name = GetObject()->GetStr(FIELD_FOLDER_NAME); for (ScribeFolder *f = NewParent->GetChildFolder(); f; f = f->GetNextFolder()) { auto n = f->GetObject()->GetStr(FIELD_FOLDER_NAME); if (n && !_stricmp(n, Name)) { Dst = f->GetFldObj(); } } if (!Dst) { // Create sub-folder if ((Dst = dynamic_cast(NewParent->GetObject()->GetStore()->Create(GetObject()->Type())))) { Dst->CopyProps(*GetObject()); if (Dst->Save(NewParent->GetObject()) == Store3Error) return Store3Error; } else return Store3Error; } if (Dst) { Copied = Store3ReplicateFolders(App, Dst, GetFldObj(), true, false, 0); } } else LAssert(!"Pointer error"); return Copied; } Store3Status ScribeFolder::SetFolder(ScribeFolder *f, int Param) { Store3Status Moved = Store3Error; if (f == this) { LAssert(0); } else if (f && GetObject() && GetObject()->GetStore() && f->GetObject() && f->GetObject()->GetStore()) { if (GetObject()->GetStore() == f->GetObject()->GetStore()) { // Simple in storage movement LArray Mv; Mv.Add(GetObject()); Moved = GetObject()->GetStore()->Move(f->GetFldObj(), Mv); if (Moved && Param >= 0) { GetObject()->SetInt(FIELD_FOLDER_INDEX, Param); } } else { // Cross storage movement... // Find or create the destinate folder LDataFolderI *Dst = 0; int Idx = 0; auto Name = GetObject()->GetStr(FIELD_FOLDER_NAME); for (ScribeFolder *c = f->GetChildFolder(); c; c = c->GetNextFolder(), Idx++) { auto n = c->GetObject()->GetStr(FIELD_FOLDER_NAME); if (n && !_stricmp(n, Name)) { Dst = c->GetFldObj(); break; } } if (!Dst) { Dst = dynamic_cast(f->GetObject()->GetStore()->Create(f->Type())); } else { ScribeFolder *c = CastFolder(dynamic_cast(Dst)); if (c) { c->Remove(); Param = Idx; DeleteObj(c); } } if (Dst) { if (View()) View()->RemoveAll(); // Clean up all the items and sub-folders, they will be created by the // replication code calling "OnNew". Thing *t; while ((t = Items[0])) { t->DecRef(); Items.Delete(t); } // Delete the child folders... ScribeFolder *Cf; while ((Cf = GetChildFolder())) { DeleteObj(Cf); } // Copy ourself over... Dst->CopyProps(*GetObject()); LDataFolderI *Old = GetFldObj(); SetObject(Dst, false, _FL); // Save the object to the new store... Store3Status s = GetObject()->Save(f->GetObject()); if (s != Store3Error) { // And replicate all the children objects... Moved = Store3ReplicateFolders(App, GetFldObj(), Old, true, true, 0); } } else LAssert(!"Not a valid folder"); } if (Moved == Store3Success) { // Move LTreeItem node... f->Insert(this, Param); Select(true); LoadFolders(); // Adjust all the other indexes so that it's remembered ScribeFolder *p = GetFolder(); if (p) { int Idx = 0; for (p = p->GetChildFolder(); p; p = p->GetNextFolder()) { p->SetSortIndex(Idx++); p->SetDirty(); } } } } else LAssert(!"Pointer error"); return Moved; } Store3Status ScribeFolder::DeleteAllThings(std::function Callback) { if (!GetFldObj()) return Store3Error; Store3Status r = GetFldObj()->DeleteAllChildren(); if (r == Store3Error) { LAssert(!"DeleteAllChildren failed."); } else if (r == Store3Success) { LAssert(Items.Length() == 0); OnUpdateUnRead(0, true); Update(); } return r; } Store3Status ScribeFolder::DeleteThing(Thing *t, std::function Callback) { Store3Status Status = Store3Error; if (t && t->GetObject()) { if (t->IsAttachment()) { #ifdef _DEBUG Mail *m = #endif t->IsAttachment()->GetOwner(); LAssert(m && Items.HasItem(m)); } if (t->GetObject()->Delete()) { t->SetParentFolder(NULL); if (View()) View()->Remove(t); } } return Status; } Store3Status ScribeFolder::WriteThing(Thing *t, std::function Callback) { if (!t) { if (Callback) Callback(Store3Error); return Store3Error; } auto ParentFolder = GetFolder(); auto Path = ParentFolder ? ParentFolder->GetPath() : NULL; auto OnAllow = [this, t, Callback]() { // Generic thing storage.. bool Create = !t->GetObject(); if (Create) t->SetObject(GetObject()->GetStore()->Create(t->Type()), false, _FL); if (!t->GetObject()) { LAssert(!"No object?"); LgiTrace("%s:%i - No object to save.\n", _FL); if (Callback) Callback(Store3Error); return; } // saving a thing that already has an item on disk auto Obj = GetObject(); auto Status = t->GetObject()->Save(Obj); if (Status != Store3Error) { // The ScribeWnd::OnNew will take care of inserting the item into the // right folder, updating the unread count, any filtering etc. t->OnSerialize(true); if (Callback) Callback(Status); } else { if (Create) t->SetObject(NULL, false, _FL); LgiTrace("%s:%i - Object->Save returned %i.\n", _FL, Status); if (Callback) Callback(Store3Error); } }; if (App) App->GetAccessLevel(App, GetWriteAccess(), Path, [OnAllow](auto Allow) { if (Allow) OnAllow(); }); else OnAllow(); return Store3Success; } int ThingContainerNameCmp(LTreeItem *a, LTreeItem *b, NativeInt d) { ScribeFolder *A = dynamic_cast(a); ScribeFolder *B = dynamic_cast(b); if (A && B) { const char *s1 = A->GetText(); const char *s2 = B->GetText(); const char *Empty = ""; return _stricmp(s1?s1:Empty, s2?s2:Empty); } else LAssert(!"Invalid objects."); return 0; } int ThingContainerIdxCmp(LTreeItem *a, LTreeItem *b, NativeInt d) { ScribeFolder *A = dynamic_cast(a); ScribeFolder *B = dynamic_cast(b); if (A && B) { int Aidx = A->GetSortIndex(); int Bidx = B->GetSortIndex(); if (Aidx >= 0 || Bidx >= 0) { return Aidx - Bidx; } } else LAssert(!"Invalid objects."); return 0; } void ScribeFolder::SortSubfolders() { int i = 0; ScribeFolder *c; LTreeItem::Items.Sort(ThingContainerNameCmp); for (c = GetChildFolder(); c; c = c->GetNextFolder()) { c->SetSortIndex(i++); c->SetDirty(); } GetTree()->UpdateAllItems(); GetTree()->Invalidate(); } void ScribeFolder::DoContextMenu(LMouse &m) { MailTree *mt = dynamic_cast(GetTree()); LScriptUi s(new LSubMenu); List Templates; bool ForceDelete = m.Shift(); bool IsTrash = false; bool IsSystem = false; if (!s.Sub) return; IsTrash = GetItemType() == MAGIC_ANY; IsSystem = App->GetFolderType(this) >= 0; if (App->GetFolderType(this) == FOLDER_OUTBOX) { s.Sub->AppendItem(LLoadString(IDS_MARK_ALL_SEND), IDM_MARK_SEND, true); } if (IsTrash) { s.Sub->AppendItem(LLoadString(IDS_EMPTY), IDM_EMPTY, true); s.Sub->AppendItem(LLoadString(IDS_MARK_ALL_READ), IDM_MARK_ALL_READ, true); } else if (!IsRoot()) { auto *LiteralNew = LLoadString(IDS_NEW); const char *Type = 0; switch (GetItemType()) { case MAGIC_MAIL: Type = LLoadString(IDS_MAIL); break; case MAGIC_CONTACT: Type = LLoadString(IDS_CONTACT); break; case MAGIC_CALENDAR: Type = LLoadString(IDS_CAL_EVENT); break; case MAGIC_FILTER: Type = LLoadString(IDS_FILTER); break; default: break; } if (Type) { char Str[256]; sprintf_s(Str, sizeof(Str), "%s %s", LiteralNew, Type); s.Sub->AppendItem(Str, IDM_NEW_EMAIL, true); } } switch (GetItemType()) { case MAGIC_CONTACT: { char Str[256]; const char *LiteralEmail = LLoadString(IDS_EMAIL); sprintf_s(Str, sizeof(Str), "%s '%s'", LiteralEmail, GetText()); s.Sub->AppendItem(Str, IDM_EMAIL_GROUP, true); ScribeFolder *f = App->GetFolder(FOLDER_TEMPLATES); if (f) { // FIXME f->LoadThings(); auto Merge = s.Sub->AppendSub(LLoadString(IDS_MERGE_TEMPLATE)); if (Merge) { int n = 0; for (auto t: f->Items) { Mail *m = t->IsMail(); if (m) { Templates.Insert(m); Merge->AppendItem(m->GetSubject()?m->GetSubject():(char*)"(no subject)", IDM_MERGE_TEMPLATE_BASE+n++, true); } } } } else { s.Sub->AppendItem(LLoadString(IDS_MERGE_TEMPLATE), 0, false); } s.Sub->AppendItem(LLoadString(IDS_MERGE_FILE), IDM_MERGE_FILE, true); break; } case MAGIC_MAIL: { if (!IsRoot()) { s.Sub->AppendItem(LLoadString(IDC_COLLECT_SUB_MAIL), IDM_COLLECT_MAIL, true); s.Sub->AppendItem(LLoadString(IDS_MARK_ALL_READ), IDM_MARK_ALL_READ, true); if (GetObject() && GetObject()->GetInt(FIELD_STORE_TYPE) == Store3Imap) { s.Sub->AppendItem(LLoadString(IDC_UNDELETE), IDM_UNDELETE, true); s.Sub->AppendItem("Expunge", IDM_EXPUNGE, true); s.Sub->AppendItem(LLoadString(IDC_REFRESH), IDM_REFRESH, true); } } break; } default: break; } if (s.Sub->ItemAt(0)) { s.Sub->AppendSeparator(); } s.Sub->AppendItem(LLoadString(IDS_FIND), IDM_FIND, true); s.Sub->AppendSeparator(); bool HasFolderTask = App->HasFolderTasks(); bool FolderOp = !HasFolderTask && !IsRoot(); s.Sub->AppendItem(LLoadString(IDS_DELETEFOLDER), IDM_DELETE, FolderOp && ((!IsTrash && !IsSystem) || ForceDelete)); s.Sub->AppendItem(LLoadString(IDS_CREATESUBFOLDER), IDM_CREATE_SUB, !HasFolderTask && CanHaveSubFolders(GetItemType())); s.Sub->AppendItem(LLoadString(IDS_RENAMEFOLDER), IDM_RENAME, FolderOp); auto ExportMimeType = GetStorageMimeType(); s.Sub->AppendItem(LLoadString(IDS_EXPORT), IDM_EXPORT, FolderOp && ExportMimeType != NULL); s.Sub->AppendSeparator(); s.Sub->AppendItem(LLoadString(IDS_SORT_SUBFOLDERS), IDM_SORT_SUBFOLDERS, true); s.Sub->AppendItem(LLoadString(IDS_COPY_PATH), IDM_COPY_PATH, true); s.Sub->AppendItem(LLoadString(IDS_PROPERTIES), IDM_PROPERTIES, true); LArray Callbacks; if (App->GetScriptCallbacks(LFolderContextMenu, Callbacks)) { LScriptArguments Args(NULL); Args[0] = new LVariant(App); Args[1] = new LVariant(this); Args[2] = new LVariant(&s); for (auto c: Callbacks) App->ExecuteScriptCallback(*c, Args); Args.DeleteObjects(); } m.ToScreen(); LPoint Scr = _ScrollPos(); m.x -= Scr.x; m.y -= Scr.y; int Msg = s.Sub->Float(GetTree(), m.x, m.y); switch (Msg) { case IDM_NEW_EMAIL: { switch (GetItemType()) { case MAGIC_MAIL: case MAGIC_CONTACT: case MAGIC_CALENDAR: case MAGIC_FILTER: { if (App) App->CreateItem(GetItemType(), this); break; } default: break; } break; } case IDM_MARK_SEND: { for (Thing *i: Items) { Mail *m = i->IsMail(); if (m) { m->SetFlags(MAIL_READY_TO_SEND | MAIL_READ | MAIL_CREATED); m->SetDirty(); m->Update(); } } break; } case IDM_EMAIL_GROUP: { if (App) { Mail *m = dynamic_cast(App->CreateItem(MAGIC_MAIL, NULL, false)); if (m) { MailUi *UI = dynamic_cast(m->DoUI()); if (UI) { List Cts; App->GetContacts(Cts, this); for (auto c: Cts) { UI->AddRecipient(c); } } } } break; } case IDM_FIND: { ScribeFolder *Folder = (ScribeFolder*) GetTree()->Selection(); OpenFinder(App, Folder); break; } case IDM_CREATE_SUB: { if (mt) mt->OnCreateSubDirectory(this); break; } case IDM_DELETE: { if (mt) mt->OnDelete(this, ForceDelete); break; } case IDM_RENAME: { auto Dlg = new FolderNameDlg(mt, GetName(true)); Dlg->DoModal([this, Dlg, mt](auto dlg, auto id) { if (id && ValidStr(Dlg->Name)) { // check for folder name conflicts... ScribeFolder *ParentFolder = GetFolder(); LString Path; if (ParentFolder) Path = ParentFolder->GetPath(); if (Path) { char s[256]; sprintf_s(s, sizeof(s), "%s/%s", Path.Get(), Dlg->Name); if (App->GetFolder(s)) { LgiMsg(mt, LLoadString(IDS_SUBFLD_NAME_CLASH), AppName, MB_OK); return; } } // change the folders name... OnRename(Dlg->Name); } delete dlg; }); break; } case IDM_EXPORT: { LString DropName = LGetLeaf(GetDropFileName()); if (!DropName) { LgiTrace("%s:%i - Failed to create folder name.\n", _FL); break; } auto s = new LFileSelect(mt); s->Name(DropName); - s->Save([&](auto dlg, auto status) + s->Save([this, ExportMimeType](auto s, auto ok) { - LAutoPtr mem(dlg); - if (status) + LAutoPtr mem(s); + if (ok) { if (LFileExists(s->Name())) { LString a, b; a.Printf(LLoadString(IDS_ERROR_FILE_EXISTS), s->Name()); b.Printf("\n%s\n", LLoadString(IDS_ERROR_FILE_OVERWRITE)); if (LgiMsg(GetTree(), a + b, AppName, MB_YESNO) == IDNO) return; } LAutoPtr f(new LFile); if (!f || !f->Open(s->Name(), O_WRITE)) { LgiTrace("%s:%i - Failed to open '%s' for writing.\n", _FL, s->Name()); return; } f->SetSize(0); LAutoPtr str(f.Release()); ExportAsync(str, ExportMimeType); } }); break; } case IDM_EMPTY: { if (App->GetMailList()) { App->GetMailList()->RemoveAll(); LArray Del; for (ScribeFolder *c = GetChildFolder(); c; c = c->GetNextFolder()) Del.Add(c->GetObject()); if (Del.Length()) GetObject()->GetStore()->Delete(Del, false); - DeleteAllThings([&](auto status) + DeleteAllThings([mt](auto status) { mt->Invalidate(); }); } break; } case IDM_SORT_SUBFOLDERS: { SortSubfolders(); break; } case IDM_PROPERTIES: { mt->OnProperties(this); break; } case IDM_COPY_PATH: { LClipBoard c(GetTree()); #ifdef WINDOWS LAutoWString w(Utf8ToWide(GetPath())); c.TextW(w); #else c.Text(GetPath()); #endif break; } case IDM_COLLECT_MAIL: { CollectSubFolderMail(); break; } case IDM_MARK_ALL_READ: { LArray Change; for (auto c : Items) { Mail *m = c->IsMail(); if (m) { if (!TestFlag(m->GetFlags(), MAIL_READ)) Change.Add(m->GetObject()); } } LVariant v = MAIL_READ; if (GetObject()->GetStore()->Change(Change, FIELD_FLAGS, v, OpPlusEquals) == Store3Error) { LProgressDlg Prog(GetTree(), 500); Prog.SetRange(Change.Length()); // FIXME!! // Prog.SetYieldTime(200); Prog.SetDescription("Marking email..."); for (auto c : Change) { auto t = CastThing(c); Mail *m = t ? t->IsMail() : NULL; if (m) m->SetFlags(m->GetFlags() | MAIL_READ, false, false); Prog++; if (Prog.IsCancelled()) break; } OnUpdateUnRead(0, true); } break; } case IDM_MERGE_FILE: { auto s = new LFileSelect(mt); s->Type("Email Template", "*.txt;*.eml"); s->Open([this](auto dlg, auto id) { if (id) { LArray Recip; for (auto i: Items) { Recip.Add(new ListAddr(i->IsContact())); } App->MailMerge(Recip, dlg->Name(), 0); Recip.DeleteObjects(); } delete dlg; }); break; } case IDM_UNDELETE: { const char *s = DomToStr(SdUndelete); GetFldObj()->OnCommand(s); break; } case IDM_EXPUNGE: { const char *s = DomToStr(SdExpunge); GetFldObj()->OnCommand(s); break; } case IDM_REFRESH: { const char *s = DomToStr(SdRefresh); GetFldObj()->OnCommand(s); break; } default: { Mail *Template = Templates[Msg - IDM_MERGE_TEMPLATE_BASE]; if (Template) { LArray Recip; for (auto i: Items) { Recip.Add(new ListAddr(i->IsContact())); } App->MailMerge(Recip, 0, Template); Recip.DeleteObjects(); } else { // Handle any installed callbacks for menu items for (unsigned i=0; iExecuteScriptCallback(Cb, Args); } } } break; } } } bool ScribeFolder::IsInTrash() { ScribeFolder *p = this; while ((p = p->GetFolder())) { if (p->GetItemType() == MAGIC_ANY) return true; } return false; } /// This just adds certain folder types as a group ware source at the app level void ScribeFolder::OnItemType() { switch (GetItemType()) { case MAGIC_CONTACT: case MAGIC_FILTER: case MAGIC_CALENDAR: case MAGIC_GROUP: App->AddThingSrc(this); break; default: break; } } bool ScribeFolder::LoadFolders() { // static int64 Last = 0; auto FldObj = GetFldObj(); if (!FldObj) return true; CurState = FldState_Loading; FoldersCurrentlyLoading++; LHashTbl, ScribeFolder*> Loaded; for (ScribeFolder *c = GetChildFolder(); c; c = c->GetNextFolder()) { Loaded.Add(c->GetObject(), c); } LDataFolderI *s; auto &Sub = FldObj->SubFolders(); for (unsigned sIdx = 0; Sub.GetState() == Store3Loaded && sIdx < Sub.Length(); sIdx++) { if (!(s = Sub[sIdx])) break; if (!Loaded.Find(s)) { ScribeFolder *n = new ScribeFolder; if (n) { n->App = App; n->SetObject(s, false, _FL); Insert(n); SetWillDirty(false); Expanded(GetOpen() != 0); SetWillDirty(true); n->LoadFolders(); n->OnItemType(); } #if 0 int64 Now = LCurrentTime(); if (Now - Last > 500) { LYield(); Last = Now; } #endif } } LTreeItem::Items.Sort(ThingContainerIdxCmp); CurState = FldState_Idle; FoldersCurrentlyLoading--; return true; } class LoadProgressItem : public LListItem { int n; int Total; int64 Last; public: LoadProgressItem(int t) { n = 0; Total = t; Last = LCurrentTime(); } void SetPos(int i) { n = i; if (n % 32 == 0) { int64 Now = LCurrentTime(); if (Now > Last + 500) { if (Parent) { Parent->Invalidate(&Pos, true); #ifdef MAC LYield(); #endif } Last = Now; } LSleep(0); } } void OnPaint(ItemPaintCtx &Ctx) { int x = n * 150 / Total; LRect &r = Ctx; Ctx.pDC->Colour(L_FOCUS_SEL_BACK); Ctx.pDC->Rectangle(r.x1, r.y1, r.x1 + x, r.y2); Ctx.pDC->Colour(L_MED); Ctx.pDC->Rectangle(r.x1 + x + 1, r.y1, r.x1 + 150, r.y2); Ctx.pDC->Colour(L_WORKSPACE); Ctx.pDC->Rectangle(r.x1 + 151, r.y1, r.x2, r.y2); } }; class LoadingItem : public LListItem { LDataIterator *Iter; LString s; public: LoadingItem(LDataIterator *it) { _UserPtr = NULL; if ((Iter = it)) { Iter->SetProgressFn([this](ssize_t pos, ssize_t sz) { this->s.Printf("%.1f%%", (double)pos * 100 / sz); this->Update(); }); } } ~LoadingItem() { if (Iter) Iter->SetProgressFn(NULL); } bool SetText(const char *str, int i) { s = str; Update(); return true; } void OnPaint(LItem::ItemPaintCtx &Ctx) { LString msg; auto loading = LLoadString(IDS_LOADING); if (s) msg.Printf("%s %s", loading, s.Get()); else msg = loading; LDisplayString d(LSysFont, msg); LSysFont->Colour(L_TEXT, L_WORKSPACE); LSysFont->Transparent(false); d.Draw(Ctx.pDC, (Ctx.X()-d.X())/2, Ctx.y1, &Ctx); } void Select(bool b) { } }; bool ScribeFolder::UnloadThings() { bool Status = true; if (IsLoaded()) { // LgiTrace("Unloading %s, Items=%i\n", GetPath(), Items.Length()); // Emptying the item list, leave the store nodes around though Thing *t; while ((t = Items[0])) { if (t->GetDirty()) { t->Save(0); } t->SetObject(NULL, false, _FL); t->DecRef(); } Items.Empty(); IsLoaded(false); GetObject()->SetInt(FIELD_LOADED, false); } return Status; } #define DEBUG_PROF_LOAD_THINGS 0 #if DEBUG_PROF_LOAD_THINGS #define PROFILE(str) prof.Add("Add"); .Add(str) #else #define PROFILE(str) #endif Store3Status ScribeFolder::LoadThings(LViewI *Parent, std::function Callback) { int OldUnRead = GetUnRead(); auto FldObj = GetFldObj(); if (!FldObj) { LgiTrace("%s:%i - No folder object.\n", _FL); return Store3Error; } if (!Parent) Parent = App; - auto ContinueLoading = [&]() + auto ContinueLoading = [this, OldUnRead, Callback, FldObj]() { WhenLoaded(_FL, - [this, OldUnRead, Callback]() + [this, OldUnRead, Callback, FldObj]() { // This is called when all the Store3 objects are loaded int Unread = OldUnRead; if (Unread < 0) Unread = GetUnRead(); Loading.Reset(); auto &Children = GetFldObj()->Children(); if (Children.GetState() != Store3Loaded) { LAssert(!"Really should be loaded by now."); return; } for (auto c = Children.First(); c; c = Children.Next()) { auto t = CastThing(c); if (t) { // LAssert(Items.HasItem(t)); } else if ((t = App->CreateThingOfType((Store3ItemTypes) c->Type(), c))) { t->SetObject(c, false, _FL); t->SetParentFolder(this); t->OnSerialize(false); } } int NewUnRead = 0; for (auto t: Items) { if (t->GetFolder() != this) { #ifdef _DEBUG char s[256]; sprintf_s(s, sizeof(s), "%s:%i - Error, thing not parented correctly: this='%s', child='%x'\n", _FL, GetText(0), t->GetObject() ? t->GetObject()->Type() : 0); printf("%s", s); LgiMsg(App, s, AppName); #endif t->SetFolder(this); } Mail *m = t->IsMail(); if (m) NewUnRead += (m->GetFlags() & MAIL_READ) ? 0 : 1; t->SetFieldArray(FieldArray); } if (Unread != NewUnRead) OnUpdateUnRead(NewUnRead - Unread, false); Update(); if (d->IsInbox < 0 && App) { d->IsInbox = App->GetFolder(FOLDER_INBOX) == this; if (d->IsInbox > 0) UpdateOsUnread(); } if (Callback) Callback(Store3Success); }, 0); if (!IsLoaded()) { bool Ui = Tree ? Tree->InThread() : false; if (Ui) Tree->Capture(false); auto &Children = FldObj->Children(); auto Status = Children.GetState(); if (Status != Store3Loaded) { if (View() && Loading.Reset(Ui ? new LoadingItem(&Children) : NULL)) View()->Insert(Loading); return Status; // Ie deferred or error... } IsLoaded(true); } return Store3Loaded; }; auto Path = GetPath(); if (!App || !Path) { LAssert(!"We should probably always have an 'App' and 'Path' ptrs..."); return Store3Error; } std::function AccessCb; if (Callback) { - AccessCb = [&](bool Access) + AccessCb = [ContinueLoading, Callback](bool Access) { if (Access) ContinueLoading(); else Callback(Store3Error); }; } auto Access = App->GetAccessLevel( Parent, GetReadAccess(), Path, AccessCb); if (Access == Store3Error) { // No read access: LgiTrace("%s:%i - Folder read access denied.\n", _FL); // Emptying the item list, leave the store nodes around though for (auto t: Items) t->SetObject(NULL, false, _FL); Items.Empty(); IsLoaded(false); Update(); } else if (Access == Store3Success) { ContinueLoading(); } return Access; } void ScribeFolder::OnRename(char *NewName) { if (!NewName) return; int FolderType = App->GetFolderType(this); SetName(NewName, true); // Calls update too.. SetDirty(); if (FolderType >= 0) { // it's a system folder so reflect the changes in // the system folder tracking options. char KeyName[32]; sprintf_s(KeyName, sizeof(KeyName), "Folder-%i", FolderType); auto Path = GetPath(); if (Path) { LVariant v = Path.Get(); App->GetOptions()->SetValue(KeyName, v); } } } void ScribeFolder::OnDelete() { if (GetObject()) { char Msg[256]; sprintf_s(Msg, sizeof(Msg), LLoadString(IDS_DELETE_FOLDER_DLG), GetText()); int Result = LgiMsg(App, Msg, AppName, MB_YESNO); if (Result == IDYES) { if (App->GetMailList()) App->GetMailList()->RemoveAll(); ScribeFolder *Parent = GetFolder(); LArray Del; Del.Add(GetObject()); if (GetObject()->GetStore()->Delete(Del, true)) { if (Parent) Parent->Select(true); } } } } ScribeFolder *ScribeFolder::GetSubFolder(const char *Path) { ScribeFolder *Folder = 0; if (Path) { if (*Path == '/') Path++; char Name[256]; char *Sep = strchr((char*)Path, '/'); if (Sep) { ZeroObj(Name); memcpy(Name, Path, Sep-Path); } else { strcpy_s(Name, sizeof(Name), Path); } LTreeItem *Starts[2] = { GetChild(), IsRoot() ? GetNext() : 0 }; for (int s=0; !Folder && sGetNext()) { ScribeFolder *f = dynamic_cast(i); if (f) { auto n = f->GetName(true); if (n.Equals(Name)) { if (Sep) Folder = f->GetSubFolder(Sep+1); else Folder = f; break; } } } } } return Folder; } bool ScribeFolder::ReindexField(int OldIndex, int NewIndex) { if (!GetFldObj()) return false; auto &Flds = GetFldObj()->Fields(); if (GetFldObj()->Fields().Length() <= 0) return false; LDataPropI *f = Flds[OldIndex]; if (!f) return false; Flds.Delete(f); Flds.Insert(f, OldIndex < NewIndex ? NewIndex - 1 : NewIndex); int i=0; FieldArray.Length(0); for (f = Flds.First(); f; f = Flds.Next()) { auto Id = f->GetInt(FIELD_ID); FieldArray[i++] = (int)Id; } for (auto t: Items) t->SetFieldArray(FieldArray); SetDirty(); return true; } int ScribeFolder::_UnreadChildren() { int Status = 0; for (ScribeFolder *f=GetChildFolder(); f; f=f->GetNextFolder()) { int u = f->GetUnRead(); if (u > 0) Status += u; Status += f->_UnreadChildren(); } return Status; } void ScribeFolder::_PourText(LPoint &Size) { if (!d) return; const char *Text = GetText(); Size.x = Size.y = 0; if (Text && !d->DsBase) { if (GetUnRead() > 0 || ChildUnRead) { const char *StartCount = strrchr(Text, '('); ssize_t NameLen = StartCount ? StartCount-Text : strlen(Text); LFont *b = (App->GetBoldFont()) ? App->GetBoldFont() : GetTree()->GetFont(); d->DsBase.Reset(new LDisplayString(b, Text, NameLen)); d->DsUnread.Reset(new LDisplayString(GetTree()->GetFont(), StartCount)); } else { d->DsBase.Reset(new LDisplayString(GetTree()->GetFont(), Text)); } } if (d->DsBase) Size.x += d->DsBase->X(); if (d->DsUnread) Size.x += d->DsUnread->X(); Size.x += 4; Size.y = MAX(16, LSysFont->GetHeight()); } void ScribeFolder::_PaintText(LItem::ItemPaintCtx &Ctx) { if (!d) return; LRect *_Text = _GetRect(TreeItemText); if (d->DsBase) { LFont *f = d->DsBase->GetFont(); int Tab = f->TabSize(); f->TabSize(0); f->Transparent(false); f->Colour(Ctx.Fore, Ctx.TxtBack); d->DsBase->Draw(Ctx.pDC, _Text->x1 + 2, _Text->y1 + 1, _Text); if (d->DsUnread) { f = d->DsUnread->GetFont(); f->Transparent(true); LColour UnreadCol(App->GetColour(L_UNREAD_COUNT)); if ( abs ( (UnreadCol.GetGray() - Ctx.Back.GetGray()) ) < 80 ) { // too close.. use fore f->Colour(Ctx.Fore, Ctx.TxtBack); } else { // contrast ok.. use unread f->Colour(UnreadCol, Ctx.TxtBack); } d->DsUnread->Draw(Ctx.pDC, _Text->x1 + 2 + d->DsBase->X(), _Text->y1 + 1); } f->TabSize(Tab); if (Ctx.x2 > _Text->x2) { Ctx.pDC->Colour(Ctx.Back); Ctx.pDC->Rectangle(_Text->x2 + 1, Ctx.y1, Ctx.x2, Ctx.y2); } } else { Ctx.pDC->Colour(Ctx.Back); Ctx.pDC->Rectangle(&Ctx); } } bool ScribeFolder::Save(ScribeFolder *Into) { bool Status = false; if (GetObject()) { // saving a disk object Store3Status s = GetObject()->Save(); if (s != Store3Error) { Status = true; SetDirty(false); } else { LAssert(0); } } return Status; } void ScribeFolder::OnExpand(bool b) { if (b && LoadOnDemand) { DeleteObj(LoadOnDemand); if (App->ScribeState != ScribeWnd::ScribeExiting) { ScribeWnd::AppState Prev = App->ScribeState; App->ScribeState = ScribeWnd::ScribeLoadingFolders; LoadFolders(); if (App->ScribeState == ScribeWnd::ScribeExiting) { LCloseApp(); return; } App->ScribeState = Prev; } } OnUpdateUnRead(0, false); SetDirty(((uchar)b) != GetOpen()); SetOpen(b); } bool ScribeFolder::OnKey(LKey &k) { #ifndef WINDOWS // This is being done by the VK_APPS key on windows... if (k.IsContextMenu()) { if (k.Down()) { LMouse m; m.x = 5; m.y = 5; m.ViewCoords = true; m.Target = GetTree(); DoContextMenu(m); } return true; } #endif return false; } #define DefField(id, wid) \ { \ LDataPropI *Fld = Fields.Create(GetFldObj()->GetStore()); \ if (Fld) \ { \ Fld->SetInt(FIELD_ID, id); \ Fld->SetInt(FIELD_WIDTH, wid); \ Fields.Insert(Fld); \ } \ } void ScribeFolder::SetDefaultFields(bool Force) { if (!GetFldObj()) return; if (Force || GetFldObj()->Fields().Length() <= 0) { LDataIterator &Fields = GetFldObj()->Fields(); Fields.DeleteObjects(); int FolderType = App ? App->GetFolderType(this) : -1; if (FolderType == FOLDER_OUTBOX || FolderType == FOLDER_SENT) { DefField(FIELD_PRIORITY, 10); DefField(FIELD_FLAGS, 15); DefField(FIELD_TO, 150); DefField(FIELD_SUBJECT, 250); DefField(FIELD_SIZE, 80); DefField(FIELD_DATE_SENT, 100); } else { switch (GetItemType()) { case MAGIC_MAIL: { DefField(FIELD_PRIORITY, 10); DefField(FIELD_FLAGS, 10); DefField(FIELD_FROM, 150); DefField(FIELD_SUBJECT, 250); DefField(FIELD_SIZE, 80); DefField(FIELD_DATE_SENT, 100); break; } case MAGIC_CONTACT: { DefField(FIELD_FIRST_NAME, 200); DefField(FIELD_LAST_NAME, 200); DefField(FIELD_EMAIL, 300); break; } case MAGIC_CALENDAR: { DefField(FIELD_CAL_SUBJECT, 200); DefField(FIELD_CAL_START_UTC, 150); DefField(FIELD_CAL_END_UTC, 150); break; } case MAGIC_FILTER: { for (int i=0; DefaultFilterFields[i]; i++) { DefField(DefaultFilterFields[i], i == 0 ? 200 : 100); } break; } case MAGIC_GROUP: { DefField(FIELD_GROUP_NAME, 300); break; } default: break; } } // set for save SetDirty(Fields.Length() > 0); } else { // already has fields, so don't interfer with them } } LString ScribeFolder::GetPath() { LString::Array p; ScribeFolder *f = this; while (f) { p.Add(f->GetName(true)); f = dynamic_cast(f->GetParent()); } LString dir = "/"; return dir + dir.Join(p.Reverse()); } void ScribeFolder::SetName(const char *Name, bool Encode) { if (Name) { d->DsBase.Reset(); d->DsUnread.Reset(); NameCache.Reset(); if (GetObject()) GetObject()->SetStr(FIELD_FOLDER_NAME, Name); SetText(Name); // Calls update } } LString ScribeFolder::GetName(bool Decode) { if (!GetObject()) return NULL; auto In = GetObject()->GetStr(FIELD_FOLDER_NAME); if (!In) return NULL; if (Decode) return LUrlDecode(In); else return In; } void ScribeFolder::Update() { if (Tree && !Tree->InThread()) { // LgiTrace("%s:%i - Update not in thread?\n", _FL); return; } d->DsBase.Reset(); d->DsUnread.Reset(); NameCache.Reset(); LTreeItem::Update(); } const char *ScribeFolder::GetText(int i) { if (!NameCache) { LString Name = GetName(true); if (!Name) return "...loading..."; size_t NameLen = strlen(Name) + 40; NameCache.Reset(new char[NameLen]); if (NameCache) { bool ShowTotals = false; LVariant v; if (App->GetOptions()->GetValue(OPT_ShowFolderTotals, v)) ShowTotals = v.CastInt32() != 0; int c = sprintf_s(NameCache, NameLen, "%s", Name.Get()); auto ItemType = GetItemType(); if (ItemType && (ShowTotals || GetUnRead() || ChildUnRead)) { c += sprintf_s(NameCache+c, NameLen-c, " ("); if (ItemType == MAGIC_MAIL || ItemType == MAGIC_ANY) { const char *Ch = ChildUnRead ? "..." : ""; if (ShowTotals) c += sprintf_s(NameCache+c, NameLen-c, "%i%s/", GetUnRead(), Ch); else if (GetUnRead()) c += sprintf_s(NameCache+c, NameLen-c, "%i%s", GetUnRead(), Ch); else if (ChildUnRead) c += sprintf_s(NameCache+c, NameLen-c, "..."); } if (ShowTotals) { if (IsLoaded()) c += sprintf_s(NameCache+c, NameLen-c, "%zi", Items.Length()); else c += sprintf_s(NameCache+c, NameLen-c, "?"); } c += sprintf_s(NameCache+c, NameLen-c, ")"); } } } return NameCache; } int ScribeFolder::GetImage(int Flags) { if (GetItemType() == MAGIC_ANY) { return ICON_TRASH; } if (Flags) { return ICON_OPEN_FOLDER; } switch (GetItemType()) { case MAGIC_MAIL: return ICON_FOLDER_MAIL; case MAGIC_CONTACT: return ICON_FOLDER_CONTACTS; case MAGIC_FILTER: return ICON_FOLDER_FILTERS; case MAGIC_CALENDAR: return ICON_FOLDER_CALENDAR; case MAGIC_NONE: return ICON_MAILBOX; default: return ICON_CLOSED_FOLDER; } } int ScribeFolder::Sizeof() { return 0; } bool ScribeFolder::Serialize(LFile &f, bool Write) { return false; } class NullMail : public Mail { Thing &operator =(Thing &c) override { return *this; } public: NullMail(ScribeWnd *App, MContainer *c, ScribeFolder *f) : Mail(App) { SetFlags(MAIL_READ | MAIL_CREATED); Container = c; SetParentFolder(f); SetSubject((char*)LLoadString(IDS_MISSING_PARENT)); } ~NullMail() { SetParentFolder(NULL); } bool IsPlaceHolder() override { return true; } const char *GetText(int i) override { // Clear all the fields return 0; } int Compare(LListItem *Arg, ssize_t Field) override { // Use the first mail to sort into position if (Container) { MContainer *c = Container->Children.Length() ? Container->Children[0] : 0; if (c && c->Message) { return c->Message->Compare(Arg, Field); } } return -1; } void OnMouseClick(LMouse &m) override { // Disable the mouse click } bool SetDirty(bool b = true) override { // Don't set it to dirty, otherwise the dirty object // clean up code will save it into the outbox. return true; } }; template int TrashCompare(T *pa, T *pb, NativeInt Data) { ScribeFolder *f = (ScribeFolder*)Data; Thing *a = dynamic_cast(pa); Thing *b = dynamic_cast(pb); if (!a || !b) return 0; int type = a->Type() - b->Type(); if (type) return type; int col = f->GetSortCol(); int *defs = a->GetDefaultFields(); if (!defs || !defs[col]) return 0; return (f->GetSortAscend() ? 1 : -1) * a->Compare(b, defs[col]); } template int TrashCompare(LListItem *pa, LListItem *pb, NativeInt Data); int ListItemCompare(LListItem *a, LListItem *b, NativeInt Data) { ScribeFolder *f = (ScribeFolder*)Data; return (f->GetSortAscend() ? 1 : -1) * a->Compare(b, f->GetSortField()); } int ThingCompare(Thing *a, Thing *b, NativeInt Data) { ScribeFolder *f = (ScribeFolder*)Data; return (f->GetSortAscend() ? 1 : -1) * a->Compare(b, f->GetSortField()); } int ThingSorter(Thing *a, Thing *b, ThingSortParams *Params) { return (Params->SortAscend ? 1 : -1) * a->Compare(b, Params->SortField); } bool ScribeFolder::Thread() { bool Status = false; if (GetItemType() == MAGIC_MAIL) { int Thread = GetThreaded(); ThingList *Lst = View(); List m; for (auto t: Items) { Mail *e = t->IsMail(); if (e) { if (Thread) { m.Insert(e); } else if (e->Container) { DeleteObj(e->Container); } } } if (Lst && m[0]) { // Thread LArray Containers; MContainer::Thread(m, Containers); LAssert(m.Length() == Items.Length()); // Insert blank items for missing thread parents for (unsigned i=0; iMessage) { LAssert(c->Children.Length() > 1); c->Message = new NullMail(App, c, this); if (c->Message) { Lst->PlaceHolders.Insert(c->Message); if (View() && c->Message) { c->Message->SetFieldArray(FieldArray); Items.Insert(c->Message); } } } } // Sort root list LArray Containers2 = Containers; ThingSortParams Params; Params.SortAscend = GetSortAscend(); Params.SortField = GetSortField(); #if 0 for (int i=0; iMessage ? Containers[i]->Message->GetFieldText(Params.SortField) : NULL; LgiTrace("%p(%s)\n", Containers[i], Str1); } Containers.Sort(ContainerCompare); LgiQuickSort(Base, Containers2.Length(), ContainerSorter, &Params); for (int i=0; iMessage ? Containers[i]->Message->GetFieldText(Params.SortField) : NULL; char *Str2 = Containers2[i]->Message ? Containers2[i]->Message->GetFieldText(Params.SortField) : NULL; LgiTrace("%p(%s), %p(%s) - %s\n", Containers[i], Str1, Containers2[i], Str2, Containers[i]!=Containers2[i]?"DIFFERENT":""); } #else MContainer **Base = &Containers[0]; if (Containers.Length() > 1) LgiQuickSort(Base, Containers.Length(), ContainerSorter, &Params); /* for (int i=0; iMessage ? Containers[i]->Message->GetFieldText(Params.SortField) : NULL; LgiTrace("%p(%s)\n", Containers[i]->Message, Str1); } LgiTrace("\n"); */ #endif // Position and index the thread tree int Index = 0; for (unsigned i=0; iPour(Index, 0, 0, i < Containers.Length()-1, &Params); } // Sort all the items by index Items.Sort(ContainerIndexer); Status = true; /* int Idx = 0; for (Thing *t = Items.First(); t; t = Items.Next(), Idx++) { Mail *m = t->IsMail(); if (m) { char *Str = m->GetFieldText(Params.SortField); LgiTrace("%i,%i %p %s\n", Idx, m->Container ? m->Container->Index : -1, m, Str); } } */ } } else { for (auto t: Items) { Mail *e = t->IsMail(); if (e) DeleteObj(e->Container); } } return Status; } struct SortPairInt { Thing *t; uint64 ts; void SetDate(Thing *th, int sf) { t = th; auto obj = th->GetObject(); // Store3State loaded = (Store3State)obj->GetInt(FIELD_LOADED); auto dt = obj->GetDate(sf); ts = dt && dt->Year() ? dt->Ts() : 0; } void SetInt(Thing *th, int sf) { t = th; auto obj = th->GetObject(); // Store3State loaded = (Store3State)obj->GetInt(FIELD_LOADED); ts = obj->GetInt(sf); } static int Compare(SortPairInt *a, SortPairInt *b) { int64_t diff = (int64_t)a->ts - (int64_t)b->ts; if (diff < 0) return -1; return diff > 0 ? 1 : 0; } }; struct SortPairStr { Thing *t; const char *ts; void SetStr(Thing *th, int sf) { t = th; ts = th->GetObject()->GetStr(sf); } static int Compare(SortPairStr *a, SortPairStr *b) { return stricmp(a->ts ? a->ts : "", b->ts ? b->ts : ""); } }; bool ScribeFolder::SortItems() { int sf = GetSortField(); LVariantType type = GV_NULL; auto StartTs = LCurrentTime(); const static int TimeOut = 2/*sec*/ * 1000; switch (sf) { case FIELD_DATE_SENT: case FIELD_DATE_RECEIVED: type = GV_DATETIME; break; case FIELD_SUBJECT: type = GV_STRING; break; default: return false; } LArray intPairs; LArray strPairs; bool intType = type == GV_DATETIME || type == GV_INT32 || type == GV_INT64; if (intType) { intPairs.Length(Items.Length()); int n = 0; auto s = intPairs.AddressOf(); if (type == GV_DATETIME) { for (auto i: Items) { s[n++].SetDate(i, sf); if (n % 50 == 0 && LCurrentTime() - StartTs >= TimeOut) return false; } } else { for (auto i: Items) { s[n++].SetInt(i, sf); if (n % 50 == 0 && LCurrentTime() - StartTs >= TimeOut) return false; } } intPairs.Sort(SortPairInt::Compare); } else if (type == GV_STRING) { strPairs.Length(Items.Length()); int n = 0; auto s = strPairs.AddressOf(); for (auto i: Items) s[n++].SetStr(i, sf); strPairs.Sort(SortPairStr::Compare); } Items.Empty(); if (intType) { if (GetSortAscend()) for (auto i: intPairs) Items.Add(i.t); else for (auto it = intPairs.rbegin(); it != intPairs.end(); it--) Items.Add((*it).t); } else { if (GetSortAscend()) for (auto i: strPairs) Items.Add(i.t); else for (auto it = strPairs.rbegin(); it != strPairs.end(); it--) Items.Add((*it).t); } return true; } void ScribeFolder::Populate(ThingList *list) { LProfile Prof("ScribeFolder::Populate", 1000); App->OnSelect(); if (!GetFldObj() || !list) return; CurState = FldState_Populating; ScribeFolder *Prev = list->GetContainer(); bool Refresh = Prev == this; // Remove old items from list Prof.Add("Delete Placeholders"); list->DeletePlaceHolders(); list->RemoveAll(); if (!Refresh || list->GetColumns() == 0 || GetItemType() == MAGIC_FILTER) { if (Prev) { // save previous folders settings Prev->SerializeFieldWidths(); } Prof.Add("Empty cols"); LVariant GridLines; if (App->GetOptions()->GetValue(OPT_GridLines, GridLines)) { list->DrawGridLines(GridLines.CastInt32() != 0); } list->EmptyColumns(); Prof.Add("Set def fields"); bool ForceDefaultFields = GetItemType() == MAGIC_FILTER; if (GetFldObj()->Fields().Length() <= 0 || ForceDefaultFields) { SetDefaultFields(ForceDefaultFields); } // Add fields to list view int n = 0; LArray Empty; for (auto t: Items) { t->SetFieldArray(Empty); } LRect *Bounds = 0; if (App->GetIconImgList()) { Bounds = App->GetIconImgList()->GetBounds(); } switch (GetItemType()) { case MAGIC_ANY: { list->AddColumn("", 170); list->AddColumn("", 170); list->AddColumn("", 170); list->AddColumn("", 170); break; } default: { n = 0; FieldArray.Length(0); for (LDataPropI *i = GetFldObj()->Fields().First(); i; i = GetFldObj()->Fields().Next()) { int FieldId = (int)i->GetInt(FIELD_ID); const char *FName = LLoadString(FieldId); int Width = (int)i->GetInt(FIELD_WIDTH); const char *FieldText = FName ? FName : i->GetStr(FIELD_NAME); LAssert(FieldText != NULL); LItemColumn *c = list->AddColumn(FieldText, Width); if (c) { switch (i->GetInt(FIELD_ID)) { case FIELD_PRIORITY: { int x = 12; if (Bounds) { x = Bounds[ICON_PRIORITY_BLACK].X() + 7; } c->Width(x); c->Image(ICON_PRIORITY_BLACK); c->Resizable(false); break; } case FIELD_FLAGS: { int x = 14; if (Bounds) { x = Bounds[ICON_FLAGS_BLACK].X() + 7; } c->Width(x); c->Image(ICON_FLAGS_BLACK); c->Resizable(false); break; } case FIELD_SIZE: { c->TextAlign(LCss::Len(LCss::AlignRight)); break; } } } FieldArray[n++] = (int)i->GetInt(FIELD_ID); } break; } } // Add all items to list if (View()) { View()->SetContainer(this); // tell the list who we are if (GetSortCol() >= 0) { // set current sort settings View()->SetSort(GetSortCol(), GetSortAscend()); } } Prof.Add("Load things"); // FIXME: LoadThings(); } // Filter List Is; // Do any threading/sorting LString SortMsg; if (!Thread() && GetSortField()) { SortMsg.Printf("Sorting " LPrintfInt64 " items", Items.Length()); Prof.Add(SortMsg); if (GetItemType() == MAGIC_ANY) { Items.Sort(TrashCompare, (NativeInt)this); } else { // Sort.. // if (!SortItems()) Items.Sort(ThingCompare, (NativeInt)this); } } // Do any filtering... Prof.Add("Filtering"); ThingFilter *Filter = App->GetThingFilter(); auto FilterStart = LCurrentTime(); size_t Pos = 0; for (auto t: Items) { t->SetFieldArray(FieldArray); if (!Filter || Filter->TestThing(t)) { // Add anyway... because all items are not part of list Is.Insert(t); } if ((LCurrentTime()-FilterStart) > 300) { if (!Loading && Loading.Reset(new LoadingItem(NULL))) list->Insert(Loading, 0); if (Loading) { LString s; s.Printf(LPrintfInt64 " of " LPrintfInt64 ", %.1f%%", Pos, Items.Length(), (double)Pos * 100 / Items.Length()); Loading->SetText(s); } FilterStart = LCurrentTime(); } Pos++; } Prof.Add("Inserting"); if (View() && Is[0]) { View()->Insert(Is, -1, true); } Prof.Add("Deleting"); list->DeletePlaceHolders(); Loading.Reset(); Prof.Add("OnSelect"); GetFldObj()->OnSelect(true); CurState = FldState_Idle; } void ScribeFolder::OnUpdateUnRead(int Offset, bool ScanItems) { if (!d->InUpdateUnread) { d->InUpdateUnread = true; int OldUnRead = GetUnRead(); d->DsBase.Reset(); d->DsUnread.Reset(); NameCache.Reset(); if (ScanItems) { if (GetItemType() == MAGIC_MAIL || GetItemType() == MAGIC_ANY) { size_t Count = 0; for (auto t: Items) { Mail *m = t->IsMail(); if (m && !TestFlag(m->GetFlags(), MAIL_READ)) Count++; } SetUnRead((int32_t)Count); } } else if (Offset != 0) { SetUnRead(GetUnRead() + Offset); } if (GetUnRead() < 0) SetUnRead(0); if (OldUnRead != GetUnRead()) { for (LTreeItem *i = GetParent(); i; i = i->GetParent()) { ScribeFolder *tc = dynamic_cast(i); if (tc && tc->GetParent()) tc->OnUpdateUnRead(0, false); } SetDirty(); if (d->IsInbox > 0) UpdateOsUnread(); } ChildUnRead = Expanded() ? 0 : _UnreadChildren(); Update(); d->InUpdateUnread = false; } } void ScribeFolder::EmptyFieldList() { if (GetFldObj()) GetFldObj()->Fields().DeleteObjects(); } void ScribeFolder::SerializeFieldWidths(bool Write) { if (GetFldObj() && View()) { // LAssert(View()->GetColumns() == Object->Fields().Length()); int Cols = MIN(View()->GetColumns(), (int)GetFldObj()->Fields().Length()); for (int i=0; iColumnAt(i); LDataPropI *f = GetFldObj()->Fields()[i]; if (c && f) { if (f->GetInt(FIELD_WIDTH) != c->Width()) { if (Write) { c->Width((int)f->GetInt(FIELD_WIDTH)); } else { f->SetInt(FIELD_WIDTH, c->Width()); SetDirty(); } } } else LAssert(0); } } } void ScribeFolder::OnProperties(int Tab) { if (!GetObject()) return; SerializeFieldWidths(); if (View()) { SetSort(View()->GetSortCol(), View()->GetSortAscending()); } OpenFolderProperties(this, Tab, [this](auto repop) { if (repop) { SetDirty(); SerializeFieldWidths(true); Populate(View()); } }); } ScribeFolder *ScribeFolder::CreateSubDirectory(const char *Name, int Type) { ScribeFolder *NewFolder = 0; auto ThisObj = dynamic_cast(GetObject()); if (Name && ThisObj && ThisObj->GetStore()) { LDataI *Fld = GetObject()->GetStore()->Create(MAGIC_FOLDER); if (Fld) { LDataFolderI *Obj = dynamic_cast(Fld); if (Obj) { NewFolder = new ScribeFolder; if (NewFolder) { NewFolder->App = App; NewFolder->SetObject(Obj, false, _FL); // Set name and type NewFolder->SetName(Name, true); NewFolder->GetObject()->SetInt(FIELD_FOLDER_TYPE, Type); ThisObj->SubFolders(); if (NewFolder->GetObject()->Save(ThisObj)) { Insert(NewFolder); NewFolder->SetDefaultFields(); NewFolder->OnItemType(); } else { DeleteObj(NewFolder); } } } } } return NewFolder; } bool ScribeFolder::Delete(LArray &Items, bool ToTrash) { if (!App) return false; List NotNew; LArray Del; LDataStoreI *Store = NULL; for (auto i: Items) { if (i->IsPlaceHolder()) { i->DecRef(); } else { Mail *m = i->IsMail(); if (m) NotNew.Insert(m); auto ObjStore = i->GetObject() ? i->GetObject()->GetStore() : NULL; if (!Store) Store = ObjStore; if (Store == ObjStore) Del.Add(i->GetObject()); else LAssert(!"All objects must have the same store."); } } if (NotNew.Length()) App->OnNewMail(&NotNew, false); if (Del.Length() == 0) return true; if (!Store) { LgiTrace("%s:%i - No Store?\n", _FL); return false; } if (!Store->Delete(Del, ToTrash)) { LgiTrace("%s:%i - Store.Delete failed.\n", _FL); return false; } return true; } -#define MoveToStatus(idx, status) if (Status) { (*Status)[idx] = (status); } - -bool ScribeFolder::MoveTo(LArray &Items, bool CopyOnly, LArray *Status) +class MoveToState { - if (Items.Length() == 0) - return false; - if (!GetObject() || !App) - return false; - - auto FolderItemType = GetItemType(); - for (unsigned i=0; i Items; + bool CopyOnly; + std::function&)> Callback; + + // Output + bool Result = false; // Overall success/failure + LArray Status; // Per item status + + // State + LArray InStoreMove; // Object in the same store... + LDataI *FolderObj = NULL; + LDataStoreI *FolderStore = NULL; + ScribeMailType NewBayesType = BayesMailUnknown; + LHashTbl, int> Map; + int NewFolderType = -1; + bool BuildDynMenus = false; + bool BayesInc = false; + size_t Moves = 0; + + // Returns true if the object is deleted. + bool SetStatus(int i, Store3Status s) { - auto t = Items[i]; - if (!t->GetObject()) + LAssert(Status[i] == Store3NotImpl); + Status[i] = s; + + LAssert(Moves > 0); + Moves--; + + if (Moves > 0) + return false; + + OnMovesDone(); + return true; + } + +public: + MoveToState(ScribeFolder *folder, + LArray &items, + bool copyOnly, + std::function&)> callback) : + Folder(folder), + Items(items), + CopyOnly(copyOnly), + Callback(callback) + { + // Validate parameters + if (Folder && + (App = Folder->App)) { - MoveToStatus(i, Store3Error); + LVariant v; + if (App->GetOptions()->GetValue(OPT_BayesIncremental, v)) + BayesInc = v.CastInt32() != 0; } else { + delete this; + return; + } + + if ((FolderObj = Folder->GetObject())) + { + FolderStore = Folder->GetObject()->GetStore(); + } + + Status.Length(Moves = Items.Length()); + for (auto &s: Status) + s = Store3NotImpl; + + auto FolderItemType = Folder->GetItemType(); + auto ThisFolderPath = Folder->GetPath(); + NewBayesType = App->BayesTypeFromPath(ThisFolderPath); + NewFolderType = App->GetFolderType(Folder); + ScribeFolder *TemplatesFolder = App->GetFolder(FOLDER_TEMPLATES); + BuildDynMenus = Folder == TemplatesFolder; + + for (unsigned i=0; iGetObject()) + { + if (SetStatus(i, Store3Error)) + return; + continue; + } + auto ThingItemType = t->Type(); - if (!(FolderItemType == ThingItemType || FolderItemType == MAGIC_ANY)) - MoveToStatus(i, Store3Error); + if (FolderItemType != ThingItemType && FolderItemType != MAGIC_ANY) + { + if (SetStatus(i, Store3Error)) + return; + continue; + } + + ScribeFolder *Old = t->GetFolder(); + LString Path; + if (Old) + { + Path = Old->GetPath(); + + if (Old == TemplatesFolder) + // Moving to or from the templates folder... update the menu + BuildDynMenus = true; + } + + if (Old && Path) + { + bool IsDeleted = false; + App->GetAccessLevel(App, + Old->GetFolderPerms(ScribeWriteAccess), + Path, + [this, i, t, &IsDeleted](bool Allow) + { + if (Allow) + IsDeleted = Move(i, t); + else + IsDeleted = SetStatus(i, Store3NoPermissions); + }); + // If the callback has already been executed and the object is deleted, exit immediately. + if (IsDeleted) + return; + } + else + { + if (Move(i, t)) + return; + } } + } - LVariant BayesInc; - if (App) - App->GetOptions()->GetValue(OPT_BayesIncremental, BayesInc); - - auto ThisFolderPath = GetPath(); - auto NewBayesType = App->BayesTypeFromPath(ThisFolderPath); - int NewFolderType = App->GetFolderType(this); - ScribeFolder *TemplatesFolder = App->GetFolder(FOLDER_TEMPLATES); - bool BuildDynMenus = this == TemplatesFolder; - LArray InStoreMove; // Object in the same store... - auto ThisStore = GetObject()->GetStore(); - LHashTbl, bool> Map; - - for (unsigned i=0; iGetFolder(); - LString Path; - if (Old) - { - Path = Old->GetPath(); - - if (Old == TemplatesFolder) - // Moving to or from the templates folder... update the menu - BuildDynMenus = true; - } - ScribeMailType OldBayesType = BayesMailUnknown; - if (BayesInc.CastInt32() && + if (BayesInc && t->IsMail() && TestFlag(t->IsMail()->GetFlags(), MAIL_READ)) { OldBayesType = App->BayesTypeFromPath(t->IsMail()); } - auto DoMove = [&]() + ScribeFolder *Old = t->GetFolder(); + Store3Status r = Store3NotImpl; + + int OldFolderType = Old ? App->GetFolderType(Old) : -1; + if ( (OldFolderType == FOLDER_TRASH || OldFolderType == FOLDER_SENT) && + NewFolderType == FOLDER_TRASH) { - int OldFolderType = Old ? App->GetFolderType(Old) : -1; - if ( (OldFolderType == FOLDER_TRASH || OldFolderType == FOLDER_SENT) && - NewFolderType == FOLDER_TRASH) + // Delete for good + r = Old ? Old->DeleteThing(t, NULL) : Store3Error; + } + else + { + // If this folder is currently selected... + if (Folder->Select()) { - // Delete for good - auto Success = Old ? Old->DeleteThing(t, NULL) : Store3Error; - MoveToStatus(i, Success ? Store3Success : Store3Error); - if (Success) - t->OnMove(); + // Insert item into list + t->SetFieldArray(Folder->FieldArray); + } + + if (CopyOnly) + { + LDataI *NewT = FolderStore->Create(t->Type()); + if (NewT) + { + NewT->CopyProps(*t->GetObject()); + r = NewT->Save(Folder->GetObject()); + } + else + { + r = Store3Error; + } } else { - // If this folder is currently selected... - if (Select()) - { - // Insert item into list - t->SetFieldArray(FieldArray); - } - - if (CopyOnly) + if (NewFolderType != FOLDER_TRASH && + OldBayesType != NewBayesType) { - LDataI *NewT = ThisStore->Create(t->Type()); - if (NewT) - { - NewT->CopyProps(*t->GetObject()); - auto s = NewT->Save(GetObject()); - MoveToStatus(i, s); - } - else - { - MoveToStatus(i, Store3Error); - } + App->OnBayesianMailEvent(t->IsMail(), OldBayesType, NewBayesType); + } + + // Move to this folder + auto o = t->GetObject(); + if (o && o->GetStore() == FolderStore) + { + InStoreMove.Add(o); + Map.Add(t, i); + r = Store3Delayed; } else { - if (NewFolderType != FOLDER_TRASH && - OldBayesType != NewBayesType) - { - App->OnBayesianMailEvent(t->IsMail(), OldBayesType, NewBayesType); - } - - // Move to this folder - auto o = t->GetObject(); - if (o && o->GetStore() == ThisStore) - { - InStoreMove.Add(o); - Map.Add(i, true); - } - else + // Out of store more... use the old single object method... for the moment.. + r = t->SetFolder(Folder); + if (r == Store3Success) { - // Out of store more... use the old single object method... for the moment.. - Store3Status s = t->SetFolder(this); - MoveToStatus(i, s); - if (s == Store3Success) - { - // Remove from the list.. - if (Old && Old->Select() && App->MailList) - App->MailList->Remove(t); - - t->OnMove(); - } - else if (s == Store3Error) - { - LgiTrace("%s:%i - SetFolder failed.\n", _FL); - } + // Remove from the list.. + if (Old && Old->Select() && App->GetMailList()) + App->GetMailList()->Remove(t); } } } - }; - - if (App) - { - App->GetAccessLevel(App, Old->GetFolderPerms(ScribeWriteAccess), Path, [&](bool Allow) - { - if (Allow) - DoMove(); - else - MoveToStatus(i, Store3NoPermissions); - }); } - else DoMove(); + + if (r == Store3Success) + t->OnMove(); + + return SetStatus(i, r); } - // FIXME: This code that runs at the end needs to wait for the DoMove events to complete somehow... - if (InStoreMove.Length()) + void OnMovesDone() { - auto Fld = dynamic_cast(GetObject()); - if (!Fld) - return false; - - auto s = ThisStore->Move(Fld, InStoreMove); - - for (unsigned i=0; i(Folder->GetObject()); + if (!Fld) + s = Store3Error; + else + s = FolderStore->Move(Fld, InStoreMove); + + for (auto p: Map) { - MoveToStatus(i, s); + Status[p.value] = s; if (s == Store3Success) { - LAssert(t->GetFolder() == this); - LAssert(Items.HasItem(t)); + LAssert(p.key->GetFolder() == Folder); + LAssert(Items.HasItem(p.key)); + + p.key->OnMove(); } } } - if (s <= Store3Error) - return false; + if (BuildDynMenus) + // Moving to or from the templates folder... update the menu + App->BuildDynMenus(); + + if (Callback) + Callback(Result, Status); + + delete this; } - - if (BuildDynMenus) - // Moving to or from the templates folder... update the menu - App->BuildDynMenus(); - - return true; +}; + +void ScribeFolder::MoveTo(LArray &Items, bool CopyOnly, std::function&)> Callback) +{ + if (Items.Length() == 0) + return; + if (!GetObject() || !App) + return; + + new MoveToState(this, Items, CopyOnly, Callback); } int ThingFilterCompare(Thing *a, Thing *b, NativeInt Data) { Filter *A = a->IsFilter(); Filter *B = b->IsFilter(); return (A && B) ? A->GetIndex() - B->GetIndex() : 0; } void ScribeFolder::ReSort() { if (View() && Select()) { View()->SetSort(GetSortCol(), GetSortAscend()); } } void ScribeFolder::SetSort(int Col, bool Ascend, bool CanDirty) { if (GetItemType() == MAGIC_FILTER) { // Remove any holes in the indexing int i = 1; Items.Sort(ThingFilterCompare); for (auto t : Items) { Filter *f = t->IsFilter(); if (f) { if (f->GetIndex() != i) { f->SetIndex(i); } i++; } } } if (GetSortCol() != Col || GetSortAscend() != (uchar)Ascend) { GetObject()->SetInt(FIELD_SORT, (Col + 1) * (Ascend ? 1 : -1)); if (CanDirty) SetDirty(); } } int ScribeFolder::GetSortField() { int Status = 0; int Col = GetSortCol(); if (Col >= 0 && Col < (int)FieldArray.Length()) Status = FieldArray[Col]; return Status; } class FolderStream : public LStream { ScribeFolder *f; // Current index into the thing array int Idx; // Total known size of stream... int64 Sz; // A memory buffer containing just the encoded 'Thing' LMemStream Mem; LString FolderMime; public: FolderStream(ScribeFolder *folder) : Mem(512 << 10) { f = folder; Idx = 0; Sz = 0; switch (f->GetItemType()) { case MAGIC_MAIL: FolderMime = sMimeMbox; break; case MAGIC_CONTACT: FolderMime = sMimeVCard; break; case MAGIC_CALENDAR: FolderMime = sMimeVCalendar; break; default: LAssert(!"Need a mime type?"); break; } } int GetIndex() { return Idx; } int Open(const char *Str = 0,int Int = 0) { return true; } bool IsOpen() { return true; } int Close() { return 0; } int64 GetSize() { return -1; } int64 SetSize(int64 Size) { return -1; } int64 GetPos() { return -1; } int64 SetPos(int64 pos) { // This means that IStream::Seek return E_NOTIMPL, which is important // for windows to support copies of unknown size. return -1; } ssize_t Read(void *Buffer, ssize_t Size, int Flags = 0) { // Read from stream.. int64 Remaining = Mem.GetSize() - Mem.GetPos(); if (Remaining <= 0) { Thing *t = Idx < (ssize_t)f->Items.Length() ? f->Items[Idx++] : NULL; if (t) { Mem.SetSize(0); // We can't allow Export to delete the Mem object, we own it. // So create a proxy object for it. LAutoPtr cp(new LProxyStream(&Mem)); if (t->Export(cp, FolderMime)) Sz += Mem.GetSize(); else LAssert(0); Mem.SetPos(0); } else return 0; } Remaining = Mem.GetSize() - Mem.GetPos(); if (Remaining > 0) { int Common = (int)MIN(Size, Remaining); ssize_t Rd = Mem.Read(Buffer, Common); if (Rd > 0) return Rd; else LAssert(0); } return 0; } ssize_t Write(const void *Buffer, ssize_t Size, int Flags = 0) { LAssert(0); return 0; } LStreamI *Clone() { LAssert(0); return NULL; } }; // ScribeFolder drag'n'drop bool ScribeFolder::GetFormats(LDragFormats &Formats) { if (GetItemType() == MAGIC_MAIL || GetItemType() == MAGIC_CONTACT || GetItemType() == MAGIC_CALENDAR) { Formats.SupportsFileStreams(); } Formats.Supports(ScribeFolderObject); return Formats.Length() > 0; } bool ScribeFolder::OnBeginDrag(LMouse &m) { if (GetParent()) Drag(Tree, m.Event, DROPEFFECT_MOVE | DROPEFFECT_COPY); return true; } void ScribeFolder::OnEndData() { DropFileName.Empty(); } LString ScribeFolder::GetDropFileName() { LAutoString Fn(MakeFileName(GetObject()->GetStr(FIELD_FOLDER_NAME), 0)); DropFileName = Fn; if (GetItemType() == MAGIC_MAIL) DropFileName += ".mbx"; else if (GetItemType() == MAGIC_CONTACT) DropFileName += ".vcf"; else if (GetItemType() == MAGIC_CALENDAR) DropFileName += ".ics"; else if (GetItemType() == MAGIC_FILTER) DropFileName += ".xml"; return DropFileName; } bool ScribeFolder::GetData(LArray &Data) { ssize_t DataSet = 0; for (unsigned idx=0; idxSetPos(0); auto Fn = GetDropFileName(); auto MimeType = sMimeMbox; auto r = dd.AddFileStream(LGetLeaf(Fn), MimeType, s); LAssert(r); } DataSet = dd.Data.Length(); } else if (dd.IsFormat(LGI_FileDropFormat)) { LMouse m; if (App->GetMouse(m, true)) { LString::Array Files; if (GetDropFileName()) { LAutoPtr f(new LFile); if (f->Open(DropFileName, O_WRITE)) { if (GetItemType() == MAGIC_MAIL) Export(AutoCast(f), sMimeMbox); else if (GetItemType() == MAGIC_CONTACT) Export(AutoCast(f), sMimeVCard); else if (GetItemType() == MAGIC_CALENDAR) Export(AutoCast(f), sMimeVCalendar); } } if (DropFileName) { Files.Add(DropFileName.Get()); if (CreateFileDrop(&dd, m, Files)) { DataSet++; } else LgiTrace("%s:%i - CreateFileDrop failed.\n", _FL); } else LgiTrace("%s:%i - No drop file name.\n", _FL); } else LgiTrace("%s:%i - GetMouse failed.\n", _FL); } else if (dd.IsFormat(ScribeFolderObject)) { ScribeClipboardFmt *Fmt = ScribeClipboardFmt::Alloc(true, 1); if (Fmt) { Fmt->FolderAt(0, this); dd.Data[0].SetBinary(Fmt->Sizeof(), Fmt); DataSet++; free(Fmt); } } } return DataSet > 0; } void ScribeFolder::CollectSubFolderMail(ScribeFolder *To) { if (!To) To = this; - LoadThings(NULL, [&](auto Status) + LoadThings(NULL, [this, To](auto Status) { LArray Items; for (auto Item: Items) { if (To != this && Item->IsMail()) Items.Add(Item); } - To->MoveTo(Items); + To->MoveTo(Items, false, NULL); for (ScribeFolder *f = GetChildFolder(); f; f = f->GetNextFolder()) { f->CollectSubFolderMail(To); } }); } void ScribeFolder::OnReceiveFiles(LArray &Files) { if (Files.Length()) { for (unsigned i=0; i f(new LTextFile); if (f->Open(File, O_READ)) Import(AutoCast(f), MimeType); } } } // Import/export bool ScribeFolder::GetFormats(bool Export, LString::Array &MimeTypes) { MimeTypes.Add(sMimeMbox); if (!Export) MimeTypes.Add(sMimeMessage); return MimeTypes.Length() > 0; } class MboxParser : public LStringPipe { LStreamI *Src = NULL; int Hdrs = 0; bool NewMsg = true; LArray Buf; int64 Pos = 0; bool Eof = false; bool IsMessageHdr(LString c) { // check that it's a from line auto parts = c.SplitDelimit(" \r"); if (parts.Length() >= 7 && parts.Length() <= 9 && parts[0].Equals("From")) { return true; } return false; } struct Blk { uint8_t *ptr; ssize_t size; }; struct Blocks : public LArray { ssize_t Bytes = 0; Blocks(LMemQueue *q) { q->Iterate([this](auto ptr, auto size) { auto &b = New(); b.ptr = ptr; b.size = size; Bytes += size; return true; }); } LString GetLine(size_t idx, size_t offset) { char buf[256]; int ch = 0; for (size_t i = idx; i < Length(); i++) { auto &b = (*this)[i]; auto p = b.ptr + offset; auto end = b.ptr + b.size; while (p < end) { if (*p == '\n' || ch == sizeof(buf)-1) return LString(buf, ch); buf[ch++] = *p++; } } return LString(); } bool ValidateSeparator(LString ln) { auto p = ln.SplitDelimit(); if (p.Length() < 7) return false; if (!p[0].Equals("From")) return false; if (p[1].Find("@") < 0) return false; bool hasYear = false; bool hasTime = false; for (int i=2; i= 1800 && val < 2200) hasYear = true; } else if (s.Find(":") > 0) { int colons = 0, nonDigits = 0; for (auto p = s.Get(); *p; p++) if (*p == ':') colons++; else if (!IsDigit(*p)) nonDigits++; if (colons == 2 && nonDigits == 0) hasTime = true; } } return hasYear && hasTime; } ssize_t FindBoundary(ssize_t start) { const char *key = "\nFrom "; const char *k = key + 1; size_t idx = 0; ssize_t offset = 0; if (Bytes == 0) return -1; if (start < 0 || start >= Bytes) { LAssert(!"Start out of range."); return -1; } // Seek to the right starting block... while (idx < Length()) { auto &b = (*this)[idx]; if (start < b.size) break; start -= b.size; offset += b.size; idx++; } // Start searching for the key... while (idx < Length()) { auto &b = (*this)[idx]; auto end = b.ptr + b.size; for (auto p = b.ptr + start; p < end; p++) { if (*k == *p) { if (*++k == 0) { // Found the "From " part, but lets check the rest of the line. // Should be in the format: // From sender date more-info auto blkAddr = (p - b.ptr) - 4; LString ln = GetLine(idx, blkAddr); if (ln && ValidateSeparator(ln)) return offset + blkAddr; } } else k = key; } offset += b.size; idx++; } return -1; } LRange FindMsg(MboxParser &parser) { auto start = FindBoundary(0); if (start > 0) LgiTrace("%s:%i - Usually the start should be 0, but it's " LPrintfSSizeT "?\n", _FL, start); if (start >= 0) { auto end = FindBoundary(start + 5); if (end > start) { return LRange(start, end - start); } else if (parser.Eof) { return LRange(start, Bytes); } } return LRange(-1, 0); } }; public: MboxParser(LStreamI *s) : LStringPipe(128 << 10) { Src = s; // _debug = true; Buf.Length(128 << 10); } bool ReadSource() { auto rd = Src->Read(Buf.AddressOf(), Buf.Length()); if (rd <= 0) { // Src stream is empty or in an error state.. Eof = true; return false; } auto wr = Write(Buf.AddressOf(), Buf.Length()); if (wr <= 0) { LgiTrace("%s:%i - Failed to write to local buffer.\n", _FL); return false; } return true; } LAutoPtr ReadMessage() { LAutoPtr m; while (true) { Blocks blks(this); auto r = blks.FindMsg(*this); if (r.Start >= 0) { LMemStream *ms = NULL; /* LgiTrace("ReadMsg " LPrintfInt64 " %s\n", Pos, r.GetStr()); auto InitPos = Pos; */ Pos += r.Len; m.Reset(ms = new LMemStream(this, r.Start, r.Len)); /* Debugging... auto key = "The package name is vmware_addons."; auto base = ms->GetBasePtr(); auto result = Strnistr(base, key, m->GetSize()); if (result) LgiTrace("Found the Key @ " LPrintfInt64 "\n", InitPos + (result - base)); */ break; } else if (!ReadSource()) { r = blks.FindMsg(*this); if (r.Start >= 0) m.Reset(new LMemStream(this, r.Start, r.Len)); break; } } return m; } }; class ImportFolderTask : public FolderTask { LDataStoreI::StoreTrans trans; LAutoPtr Parser; public: ImportFolderTask(ScribeFolder *fld, LAutoPtr in, LString mimeType, ThingType::IoProgressCallback cb) : FolderTask(fld, in, mimeType, cb) { SetDescription(LLoadString(IDS_MBOX_READING)); SetType("K"); SetScale(1.0/1024.0); SetRange(Stream->GetSize()); trans = fld->GetObject()->GetStore()->StartTransaction(); } bool TimeSlice() { auto Start = LCurrentTime(); bool Eof = false; if (!Parser) Parser.Reset(new MboxParser(Stream)); while ( Parser && LCurrentTime() - Start < WORK_SLICE_MS && !IsCancelled()) { auto Msg = Parser->ReadMessage(); if (Msg) { Mail *m = dynamic_cast(App->CreateItem(MAGIC_MAIL, Folder, false)); if (m) { m->OnAfterReceive(Msg); m->SetFlags(MAIL_RECEIVED|MAIL_READ); m->Save(); m->Update(); } else { Eof = true; break; } } else { Eof = true; break; } Value(Stream->GetPos()); } return !Eof; } }; ThingType::IoProgress ScribeFolder::Import(IoProgressImplArgs) { if (Stricmp(mimeType, sMimeMbox) == 0 || Stricmp(mimeType, "text/x-mail") == 0) { // Mail box format... ThingType::IoProgress p(Store3Delayed); p.prog = new ImportFolderTask(this, stream, mimeType, cb); return p; } else if (Stricmp(mimeType, sMimeVCard) == 0) { VCard Io; Thing *t; bool Error = false; int Imported = 0; int Idx = 0; while ((t = App->CreateItem(GetItemType(), 0, false))) { Contact *c = t->IsContact(); if (!c) { t->DecRef(); Error = true; break; } if (Io.Import(c->GetObject(), stream)) { const char *First = 0, *Last = 0; c->GetField(FIELD_FIRST_NAME, First); c->GetField(FIELD_LAST_NAME, Last); LgiTrace("Import %i %s %s\n", Idx, First, Last); if (t->Save(this)) { Imported++; } else { Error = true; break; } } else { t->DecRef(); break; } Idx++; } if (Error) { LgiMsg( App, LLoadString(IDS_ERROR_IMPORT_COUNT), AppName, MB_OK, LLoadString(IDC_CONTACTS), Imported); IoProgressError("Contact import error."); } IoProgressSuccess(); } else if (Stricmp(mimeType, sMimeVCalendar) == 0) { VCal Io; Thing *t; while ((t = App->CreateItem(GetItemType(), this, false))) { if (Io.Import(t->GetObject(), stream)) { if (!t->Save(this)) IoProgressError("Contact save failed."); } else { t->DecRef(); break; } } IoProgressSuccess(); } else if (GetObject()) { Thing *t = App->CreateThingOfType(GetItemType(), GetObject()->GetStore()->Create(GetItemType())); if (!t) IoProgressError("Failed to create contact"); if (!t->Import(stream, mimeType)) { t->DecRef(); IoProgressError("Contact import failed."); } if (!t->Save(this)) { t->DecRef(); IoProgressError("Contact save failed."); } IoProgressSuccess(); } IoProgressNotImpl(); } class ExportFolderTask : public FolderTask { int Idx = 0; public: ExportFolderTask( ScribeFolder *folder, LAutoPtr out, LString mimeType, ThingType::IoProgressCallback cb) : FolderTask(folder, out, mimeType, cb) { bool Mbox = _stricmp(MimeType, sMimeMbox) == 0; // Clear the files contents Stream->SetSize(0); // Setup progress UI SetDescription(Mbox ? LLoadString(IDS_MBOX_WRITING) : (char*)"Writing..."); SetRange(Folder->Items.Length()); switch (Folder->GetItemType()) { case MAGIC_MAIL: SetType(LLoadString(IDS_EMAIL)); break; case MAGIC_CALENDAR: SetType(LLoadString(IDS_CALENDAR)); break; case MAGIC_CONTACT: SetType(LLoadString(IDS_CONTACT)); break; case MAGIC_GROUP: SetType("Groups"); break; default: SetType("Objects"); break; } SetPulse(PULSE_MS); SetAlwaysOnTop(true); } bool TimeSlice() { auto Start = LCurrentTime(); while ( LCurrentTime() - Start < WORK_SLICE_MS && !IsCancelled()) { if (Idx >= (ssize_t)Folder->Items.Length()) return false; // Process all the container's items Thing *t = Folder->Items[Idx++]; if (!t) return false; LAutoPtr wrapper(new LProxyStream(Stream)); if (!t->Export(wrapper, MimeType)) { Status.status = Store3Error; Status.errMsg = "Error exporting items."; return false; } Value(Idx); } return true; } /* void OnPulse() { LProgressDlg::OnPulse(); PostEvent(M_EXPORT_NEXT); } LMessage::Result OnEvent(LMessage *Msg) { if (Msg->Msg() == M_EXPORT_NEXT) { auto StartTs = LCurrentTime(); while ( !IsCancelled() && (LCurrentTime() - StartTs) < WORK_SLICE_MS) { if (Idx >= (ssize_t)Folder->Items.Length()) { Quit(); break; } // Process all the container's items Thing *t = Folder->Items[Idx++]; if (t) { LAutoPtr wrapper(new LProxyStream(Out)); if (t->Export(wrapper, MimeType)) { Value(Idx); } else { Status.status = Store3Error; Status.errMsg = "Error exporting items."; if (!onComplete) LgiMsg(this, "%s", AppName, MB_OK, Status.errMsg.Get()); Quit(); } } } return 0; } return LProgressDlg::OnEvent(Msg); } */ }; class FolderExportTask : public LProgressDlg { LAutoPtr Out; ScribeFolder *Folder; LString MimeType; int Idx; bool HasError = false; public: // Minimum amount of time to do work. constexpr static int WORK_SLICE_MS = 130; // This should be larger then WORK_SLICE_MS to allow message loop to process constexpr static int PULSE_MS = 200; FolderExportTask(LAutoPtr out, ScribeFolder *folder, LString mimeType) : LProgressDlg(folder->App) { Out = out; Folder = folder; MimeType = mimeType; Idx = 0; Ts = LCurrentTime(); Folder->App->OnFolderTask(this, true); bool Mbox = _stricmp(MimeType, sMimeMbox) == 0; // Clear the files contents Out->SetSize(0); // Setup progress UI SetDescription(Mbox ? LLoadString(IDS_MBOX_WRITING) : (char*)"Writing..."); SetRange(Folder->Items.Length()); switch (Folder->GetItemType()) { case MAGIC_MAIL: SetType(LLoadString(IDS_EMAIL)); break; case MAGIC_CALENDAR: SetType(LLoadString(IDS_CALENDAR)); break; case MAGIC_CONTACT: SetType(LLoadString(IDS_CONTACT)); break; case MAGIC_GROUP: SetType("Groups"); break; default: SetType("Objects"); break; } SetPulse(PULSE_MS); SetAlwaysOnTop(true); } ~FolderExportTask() { Folder->App->OnFolderTask(this, false); } bool OnRequestClose(bool OsClose) { return true; } void OnPulse() { LProgressDlg::OnPulse(); PostEvent(M_EXPORT_NEXT); } LMessage::Result OnEvent(LMessage *Msg) { if (Msg->Msg() == M_EXPORT_NEXT) { auto StartTs = LCurrentTime(); while ( !IsCancelled() && (LCurrentTime() - StartTs) < WORK_SLICE_MS) { if (Idx >= (ssize_t)Folder->Items.Length()) { Quit(); break; } // Process all the container's items Thing *t = Folder->Items[Idx++]; if (t) { if (t->Export(Out, MimeType, NULL)) { Value(Idx); } else { HasError = true; LgiMsg(this, "Error exporting items.", AppName); Quit(); } } } return 0; } return LProgressDlg::OnEvent(Msg); } }; // This is the mime type used to storage objects on disk const char *ScribeFolder::GetStorageMimeType() { auto Type = GetItemType(); switch (Type) { case MAGIC_MAIL: return sMimeMbox; case MAGIC_CALENDAR: return sMimeVCalendar; case MAGIC_CONTACT: return sMimeVCard; case MAGIC_FILTER: return sMimeXml; default: LgiTrace("%s:%i - Unsupported storage type: %s\n", _FL, Store3ItemTypeName(Type)); break; } return NULL; } void ScribeFolder::ExportAsync(LAutoPtr f, const char *MimeType, std::function Callback) { if (!MimeType) { LAssert(!"No Mimetype"); if (Callback) Callback(NULL); return; } - LoadThings(NULL, [&](auto Status) - { - FolderExportTask *Task = NULL; - if (Status == Store3Success) - Task = new FolderExportTask(f, this, MimeType); - if (Callback) - Callback(Task); - }); + LoadThings( NULL, + [ + this, + Str = f.Release(), + MimeType = LString(MimeType), + Callback + ] + (auto Status) + { + LAutoPtr f(Str); + FolderExportTask *Task = NULL; + if (Status == Store3Success) + Task = new FolderExportTask(f, this, MimeType); + if (Callback) + Callback(Task); + }); } ThingType::IoProgress ScribeFolder::Export(IoProgressImplArgs) { IoProgress ErrStatus(Store3Error); if (!mimeType) { ErrStatus.errMsg = "No mimetype."; if (cb) cb(&ErrStatus, NULL); return ErrStatus; } if (!LoadThings()) { ErrStatus.errMsg = "Failed to load things."; if (cb) cb(&ErrStatus, NULL); return ErrStatus; } IoProgress Status(Store3Delayed); Status.prog = new ExportFolderTask(this, stream, mimeType, cb); return Status; } size_t ScribeFolder::Length() { if (GetItemType() == MAGIC_MAIL) { ThingList *v = View(); if (v) return v->Length(); } if (IsLoaded()) return Items.Length(); return GetItems(); } ssize_t ScribeFolder::IndexOf(Mail *m) { if (GetItemType() == MAGIC_MAIL) { ThingList *v = View(); if (v) return v->IndexOf(m); return Items.IndexOf(m); } return -1; } Mail *ScribeFolder::operator [](size_t i) { if (GetItemType() == MAGIC_MAIL) { ThingList *v = View(); if (v) return dynamic_cast(v->ItemAt(i)); Thing *t = Items[i]; if (t) return t->IsMail(); } return NULL; } bool ScribeFolder::GetVariant(const char *Name, LVariant &Value, const char *Array) { ScribeDomType Fld = StrToDom(Name); switch (Fld) { case SdType: // Type: Int32 { Value = GetObject()->Type(); break; } case SdName: // Type: String { Value = GetName(true).Get(); break; } case SdPath: // Type: String { auto p = GetPath(); if (p) Value = p; else return false; break; } case SdUnread: // Type: Int32 { Value = GetUnRead(); break; } case SdLength: // Type: Int32 { Value = (int32)Items.Length(); break; } case SdItem: // Type: Thing[] { Value.Empty(); LoadThings(); // Use in sync mode, no callback // This call back HAS to set value one way or another... if (Array) { bool IsNumeric = true; for (auto *v = Array; *v; v++) { if (!IsDigit(*v)) { IsNumeric = false; break; } } if (IsNumeric) { int Idx = atoi(Array); if (Idx >= 0 && Idx < (ssize_t)Items.Length()) { Value = (LDom*) Items[Idx]; return true; } } else // Is message ID? { for (auto t : Items) { Mail *m = t->IsMail(); if (!m) break; auto Id = m->GetMessageId(); if (Id && !strcmp(Id, Array)) { Value = (LDom*)t; return true; } } } } else if (Value.SetList()) { for (auto t : Items) Value.Value.Lst->Insert(new LVariant((LDom*)t)); return true; } break; } case SdItemType: // Type: Int32 { Value = GetItemType(); break; } case SdScribe: // Type: ScribeWnd { Value = (LDom*)App; break; } case SdChild: // Type: ScribeFolder { Value = (LDom*)GetChildFolder(); break; } case SdNext: // Type: ScribeFolder { Value = (LDom*)GetNextFolder(); break; } case SdSelected: // Type: Thing[] { if (!Select() || !Value.SetList()) return false; List a; if (!App->GetMailList()->GetSelection(a)) return false; for (auto t: a) { Value.Value.Lst->Insert(new LVariant(t)); } break; } case SdExpanded: // Type: Boolean { Value = Expanded(); break; } default: { return false; } } return true; } bool ScribeFolder::SetVariant(const char *Name, LVariant &Value, const char *Array) { ScribeDomType Fld = StrToDom(Name); switch (Fld) { case SdName: // Type: String { char *s = Value.CastString(); if (ValidStr(s)) OnRename(s); else return false; break; } case SdExpanded: // Type: Boolean { Expanded(Value.CastInt32() != 0); break; } default: return false; } return true; } bool ScribeFolder::CallMethod(const char *MethodName, LVariant *ReturnValue, LArray &Args) { ScribeDomType m = StrToDom(MethodName); switch (m) { case SdLoad: // Type: () { - LoadThings(App, [&](auto Status) - { - *ReturnValue = Status == Store3Success; - }); - - // Convert the async LoadFolders call back to sync. - WaitForVariant(*ReturnValue); + LoadThings(App); + // FIXME: Callback for status? return true; } case SdSelect: // Type: () { Select(true); return true; } case SdImport: // Type: (String FileName, String MimeType) { *ReturnValue = false; if (Args.Length() != 2) LgiTrace("%s:%i - Error: expecting 2 arguments to 'Import'.\n", _FL); else { auto FileName = Args[0]->Str(); LAutoPtr f(new LFile); if (f->Open(FileName, O_READ)) { auto p = Import(AutoCast(f), Args[1]->Str()); *ReturnValue = p.status; } else LgiTrace("%s:%i - Error: Can't open '%s' for reading.\n", _FL, FileName); } break; } case SdExport: // Type: (String FileName, String MimeType) { *ReturnValue = false; if (Args.Length() != 2) LgiTrace("%s:%i - Error: expecting 2 arguments to 'Export'.\n", _FL); else { auto FileName = Args[0]->Str(); LAutoPtr f(new LFile); if (f->Open(FileName, O_WRITE)) { auto p = Export(AutoCast(f), Args[1]->Str()); *ReturnValue = p.status; } else LgiTrace("%s:%i - Error: Can't open '%s' for writing.\n", _FL, FileName); } break; } default: break; } return false; } void ScribeFolder::OnRethread() { if (GetThreaded()) { Thread(); } } diff --git a/Code/ScribeFolderDlg.cpp b/Code/ScribeFolderDlg.cpp --- a/Code/ScribeFolderDlg.cpp +++ b/Code/ScribeFolderDlg.cpp @@ -1,229 +1,229 @@ #include "Scribe.h" #include "ScribeFolderDlg.h" #include "resdefs.h" #include "lgi/common/LgiRes.h" #include "lgi/common/FileSelect.h" //////////////////////////////////////////////////////////////////////////// class RecentItem : public LListItem { ScribeFolderDlg *Dlg; public: RecentItem(ScribeFolderDlg *dlg, char *FileName) { Dlg = dlg; SetText(FileName); } void OnSelect() { Dlg->SetCtrlName(IDC_EXISTING_FOLDER, GetText(0)); } void OnMouseClick(LMouse &m) { if (Dlg && m.Double()) { Dlg->PostEvent(M_CHANGE, IDOK); } } }; //////////////////////////////////////////////////////////////////////////// ScribeFolderDlg::ScribeFolderDlg(ScribeWnd *app) { SetParent(App = app); Create = false; if (LoadFromResource(IDD_NO_FOLDER)) { // Load MRU LList *Recent; if (GetViewById(IDC_FOLDER_HISTORY, Recent)) { for (int i=0; i<10; i++) { char Key[32]; LVariant f; sprintf_s(Key, sizeof(Key), "FolderMru.%i", i); if (App->GetOptions()->GetValue(Key, f)) { Recent->Insert(new RecentItem(this, f.Str())); } } } // Look at folders... LArray Current; int Exists = 0; LXmlTag *MailStores = App->GetOptions()->LockTag(OPT_MailStores, _FL); if (MailStores) { for (auto t: MailStores->Children) { char *File = t->GetAttr(OPT_MailStoreLocation); if (ValidStr(File)) { Current.Add(NewStr(File)); Exists += LFileExists(File) ? 1 : 0; } } App->GetOptions()->Unlock(); } if (Current.Length()) { char Msg[256]; if (Exists) { // another app has it open SetCtrlValue(IDC_ACTION, 1); sprintf_s(Msg, sizeof(Msg), LLoadString(IDS_ERROR_CANT_OPEN_FOLDERS), Current[0]); SetCtrlName(IDC_MESSAGE, Msg); } else { // doesn't exist... SetCtrlValue(IDC_ACTION, 0); sprintf_s(Msg, sizeof(Msg), LLoadString(IDS_ERROR_FOLDERS_DONT_EXIST), Current[0]); SetCtrlName(IDC_MESSAGE, Msg); SetCtrlName(IDC_NEW_FOLDER, Current[0]); } } else { // no file actually specified... this is most likely because it's // a new installation. char Def[MAX_PATH_LEN] = ""; LAutoString Base = App->GetDataFolder(); if (LMakePath(Def, sizeof(Def), Base, "Folders.mail3")) SetCtrlName(IDC_NEW_FOLDER, Def); else LgiTrace("%s:%i - LMakePath failed.\n", _FL); SetCtrlValue(IDC_ACTION, 0); SetCtrlName(IDC_MESSAGE, LLoadString(IDS_ENTER_FILE_NAME_FOR_FOLDERS)); } Current.DeleteArrays(); LViewI *w = FindControl(IDC_ACTION); LNotification note(LNotifyValueChanged); if (w) OnNotify(w, note); } MoveToCenter(); } int ScribeFolderDlg::OnNotify(LViewI *Ctrl, LNotification n) { switch (Ctrl->GetId()) { case IDC_BROWSE_NEW: { auto Select = new LFileSelect(this); auto Txt = GetCtrlName(IDC_NEW_FOLDER); if (Txt) { char Def[MAX_PATH_LEN]; strcpy_s(Def, sizeof(Def), Txt); LTrimDir(Def); Select->InitialDir(Def); } else { Select->InitialDir(LGetExePath()); } Select->Type("v3 Mail Store", "*.sqlite"); Select->Type("All Files", LGI_ALL_FILES); - Select->Save([&](auto dlg, auto status) + Select->Save([this](auto s, auto ok) { - if (status) + if (ok) { char Def[MAX_PATH_LEN]; - strcpy_s(Def, sizeof(Def), Select->Name()); + strcpy_s(Def, sizeof(Def), s->Name()); char *d = strrchr(Def, DIR_CHAR); if (d) { char *e = strrchr(d, '.'); if (!e) { - if (Select->SelectedType() == 0) + if (s->SelectedType() == 0) strcat(d, ".mail3"); } SetCtrlName(IDC_NEW_FOLDER, Def); } else LgiMsg(this, "Error: Invalid path.", AppName); } - delete dlg; + delete s; }); break; } case IDC_BROWSE_EXISTING: { auto Select = new LFileSelect(this); char Def[300]; auto Txt = GetCtrlName(IDC_EXISTING_FOLDER); if (Txt) { strcpy_s(Def, sizeof(Def), Txt); LTrimDir(Def); Select->InitialDir(Def); } else { Select->InitialDir(LGetExePath()); } Select->Type("v3 Mail Store", "*.sqlite"); Select->Type("All Files", LGI_ALL_FILES); Select->Open([this](auto dlg, auto id) { if (id) { if (LFileExists(dlg->Name()) || LDirExists(dlg->Name())) SetCtrlName(IDC_EXISTING_FOLDER, dlg->Name()); else LgiMsg(this, LLoadString(IDS_ERROR_FOLDERS_DONT_EXIST), AppName, MB_OK, dlg->Name()); } delete dlg; }); break; } // case IDC_ACTION: case IDC_CREATE_FOLDER: case IDC_EXISTING: { Create = GetCtrlValue(IDC_CREATE_FOLDER) != 0; SetCtrlEnabled(IDC_NEW_FOLDER, Create); SetCtrlEnabled(IDC_BROWSE_NEW, Create); SetCtrlEnabled(IDC_EXISTING_FOLDER, !Create); SetCtrlEnabled(IDC_BROWSE_EXISTING, !Create); SetCtrlEnabled(IDC_FOLDER_HISTORY, !Create); break; } case IDOK: { Create = GetCtrlValue(IDC_CREATE_FOLDER) != 0; FolderFile.Reset(NewStr(GetCtrlName(Create ? IDC_NEW_FOLDER : IDC_EXISTING_FOLDER))); LgiTrace("In IDOK, FolderFile=%s\n", FolderFile.Get()); // fall thru } case IDCANCEL: { EndModal(Ctrl->GetId()); break; } } return 0; } diff --git a/Code/ScribeFolderProp.cpp b/Code/ScribeFolderProp.cpp --- a/Code/ScribeFolderProp.cpp +++ b/Code/ScribeFolderProp.cpp @@ -1,372 +1,375 @@ /* ** FILE: ScribeFolderProp.cpp ** AUTHOR: Matthew Allen ** DATE: 17/5/1999 ** DESCRIPTION: Scribe folder properties dialog ** ** Copyright (C) 1999, Matthew Allen ** fret@memecode.com */ #include #include #include #include #include "Scribe.h" #include "lgi/common/TextLabel.h" #include "lgi/common/ProgressDlg.h" #include "resdefs.h" #include "lgi/common/TabView.h" #include "lgi/common/LgiRes.h" ////////////////////////////////////////////////////////////////////////////// class LFolderInfo : public LListItem { public: ScribeFolder *Folder; uint64 Size; }; int FolderInfo_Compare(LListItem *a, LListItem *b, NativeInt Data) { LFolderInfo *A = dynamic_cast(a); LFolderInfo *B = dynamic_cast(b); if (A && B) { return (int) ((int64)B->Size - (int64)A->Size); } return 0; } ////////////////////////////////////////////////////////////////////////////// #define M_INIT_DONE (M_USER + 1000) class FolderPropertiesDlg : public LDialog { // Data ScribeFolder *Folder; // Controls LTabView *Tab; // LTabPage *DetailTab; LList *Usage; LView *Txt; // Scanning portion.. bool Loop; public: bool RePopulate; FolderPropertiesDlg(ScribeFolder *folder, int InitialTab) { RePopulate = false; Loop = true; Txt = 0; Folder = folder; if (!Folder) { LAssert(!"No folder."); return; } SetParent(Folder->App); if (LoadFromResource(IDD_FOLDER_PROPS)) { MoveToCenter(); GetViewById(IDC_TAB, Tab); GetViewById(IDC_FOLDER_MSG, Txt); GetViewById(IDC_USAGE, Usage); } auto Path = Folder->GetPath(); SetCtrlName(IDC_PATH, Path); ScribePerm p = Folder->GetFolderPerms(ScribeReadAccess); if (p == PermRequireAdmin) { SetCtrlEnabled(IDC_FPR_NONE, false); SetCtrlEnabled(IDC_FPR_USER, false); } else { SetCtrlEnabled(IDC_FPR_ADMIN, false); } SetCtrlValue(IDC_FOLDER_READ, p); p = Folder->GetFolderPerms(ScribeWriteAccess); if (p == PermRequireAdmin) { SetCtrlEnabled(IDC_FPW_NONE, false); SetCtrlEnabled(IDC_FPW_USER, false); } else { SetCtrlEnabled(IDC_FPW_ADMIN, false); } SetCtrlValue(IDC_FOLDER_WRITE, p); } ~FolderPropertiesDlg() { LAssert(Loop == false); } void OnCreate() { PostEvent(M_INIT_DONE); } bool OnRequestClose(bool OsClose) { if (Loop) { Loop = false; return false; } return true; } int OnNotify(LViewI *Ctrl, LNotification n) { switch (Ctrl->GetId()) { case IDOK: { - auto Err = [&]() + auto Err = [this]() { LgiMsg(this, "Failed to set permissions.", AppName); return false; }; - Folder->SetFolderPerms(this, ScribeReadAccess, (ScribePerm) GetCtrlValue(IDC_FOLDER_READ), [&](auto status) + Folder->SetFolderPerms( this, + ScribeReadAccess, + (ScribePerm) GetCtrlValue(IDC_FOLDER_READ), + [this, Err](auto status) { if (!status) return Err(); - this->Folder->SetFolderPerms(this, ScribeWriteAccess, (ScribePerm) this->GetCtrlValue(IDC_FOLDER_WRITE), [&](auto status) + this->Folder->SetFolderPerms(this, ScribeWriteAccess, (ScribePerm) this->GetCtrlValue(IDC_FOLDER_WRITE), [this, Err](auto status) { if (!status) return Err(); EndModal(1); return true; }); return true; }); break; } case IDCANCEL: { if (Loop) Loop = false; else EndModal(0); break; } } return 0; } void AddMimeSeg(LDataPropI *Ptr, Counter &c, uint64 Size) { // Add this one... LDataI *Seg = dynamic_cast(Ptr); if (Seg) { c.Inc(Seg->Type()); Size += Seg->Size(); // Add the children... LDataIt Children = Seg->GetList(FIELD_MIME_SEG); if (Children) { for (LDataPropI *Child = Children->First(); Child; Child = Children->Next()) { AddMimeSeg(Child, c, Size); } } } } void Count(LDataFolderI *f, Counter &c, int depth = 0) { uint64 Size = f->Size(); c.Inc(f->Type()); LDataIterator &fc = f->Children(); for (unsigned i=0; Loop && iType()); Size += t->Size(); if (t->Type() == MAGIC_MAIL) { AddMimeSeg(t->GetObj(FIELD_MIME_SEG), c, Size); } } if (i % 30 == 0) LYield(); } c.Add(1, Size); #ifdef _DEBUG char s[256]; memset(s, '\t', depth); s[depth] = 0; LgiTrace("%sCounting %s (size=%i)\n", s, f->GetStr(FIELD_FOLDER_NAME), Size); #endif for (unsigned n=0; Loop && nSubFolders().Length(); n++) { LDataFolderI *s = f->SubFolders()[n]; Count(s, c, depth + 1); LYield(); } } void Run() { Counter c; LYield(); // Do count LDataFolderI *f = Folder->GetFldObj(); c.Inc(f->Type()); c.Add(1, f->Size()); LDataIterator &Children = f->Children(); for (unsigned i=0; Loop && iType()); c.Add(1, t->Size()); LYield(); } for (ScribeFolder *Child = Folder->GetChildFolder(); Loop && Child; Child = Child->GetNextFolder()) { uint64 Old = c.GetTypeCount(1); Count(Child->GetFldObj(), c); if (Usage) { LFolderInfo *i = new LFolderInfo; if (i) { i->Folder = Child; i->Size = c.GetTypeCount(1) - Old; char Size[32]; LFormatSize(Size, sizeof(Size), i->Size); i->SetText(Child->GetText(), 0); i->SetText(Size, 1); Usage->Insert(i); } } LYield(); } int64 Used = c.GetTypeCount(1); // 64 bytes in the header // post count tallying if (Loop && Usage) { // set the percent for (auto it = Usage->begin(); Loop && it != Usage->end(); it++) { LFolderInfo *i = dynamic_cast(*it); if (!i) continue; char Str[32]; sprintf_s(Str, sizeof(Str), "%.1f", (double)(int64)i->Size * 100 / Used ); i->SetText(Str, 2); LYield(); } // sort the items Usage->Sort(FolderInfo_Compare); Usage->ResizeColumnsToContent(); } // other props LString MsgSize = LFormatSize(Used); char Msg[512]; const char *Format = LLoadString(IDS_FOLDER_PROPERTIES_DLG); int ch = sprintf_s(Msg, sizeof(Msg), Format?Format:"", (int) c.GetTypeCount(MAGIC_MAIL), (int) c.GetTypeCount(MAGIC_CONTACT), (int) c.GetTypeCount(MAGIC_FOLDER), MsgSize.Get()); if (Loop && !Folder->GetParent()) { LMailStore *Ms = Folder->App->GetDefaultMailStore(); if (Ms) { char File[32], Unused[32]; uint64 FileSize = Ms->Store->Size(); LFormatSize(File, sizeof(File), FileSize); LFormatSize(Unused, sizeof(Unused), FileSize-Used); sprintf_s(Msg+ch, sizeof(Msg)-ch, LLoadString(IDS_FOLDER_PROPERTIES_COMPACT), File, (int) ((Used*100)/FileSize), Unused); } } if (Txt) { Txt->Name(Msg); Txt->SendNotify(LNotifyTableLayoutRefresh); } Loop = false; } LMessage::Param OnEvent(LMessage *Msg) { if (Msg->Msg() == M_INIT_DONE) Run(); return LDialog::OnEvent(Msg); } }; ////////////////////////////////////////////////////////////////////////////// void OpenFolderProperties(ScribeFolder *Parent, int Tab, std::function callback) { auto Dlg = new FolderPropertiesDlg(Parent, Tab); Dlg->DoModal([callback, Dlg](auto dlg, auto code) { if (code && callback) callback(Dlg->RePopulate); delete dlg; }); } diff --git a/Code/ScribeFolderTree.cpp b/Code/ScribeFolderTree.cpp --- a/Code/ScribeFolderTree.cpp +++ b/Code/ScribeFolderTree.cpp @@ -1,826 +1,827 @@ /* ** FILE: ScribeFolderTree.cpp ** AUTHOR: Matthew Allen ** DATE: 17/1/2000 ** DESCRIPTION: Scribe Folder Tree ** ** Copyright (C) 2000-2002, Matthew Allen ** fret@memecode.com */ // Includes #include "Scribe.h" #include "lgi/common/DropFiles.h" #include "lgi/common/TextLabel.h" #include "lgi/common/RtfHtml.h" #include "resdefs.h" #include "ScribeListAddr.h" #include "lgi/common/ProgressDlg.h" #if WINNATIVE #include "OutlookDropSupport.cpp" #endif #include "lgi/common/LgiRes.h" ////////////////////////////////////////////////////////////////////////////// int FolderSorter(ScribeFolder *a, ScribeFolder *b, int d) { auto A = a->GetName(true); auto B = b->GetName(true); return A && B ? _stricmp(A, B) : 0; } ////////////////////////////////////////////////////////////////////////////// MailTree::MailTree(ScribeWnd *app) : LTree(100, 0, 0, 100, 100, "") { App = app; LastHit = 0; LastWasRoot = -1; Sunken(false); } MailTree::~MailTree() { } void MailTree::OnCreate() { SetWindow(this); } ssize_t MailTree::Sizeof() { LAssert(0); return 0; } bool MailTree::Serialize(LFile &f, bool Write) { bool Status = false; LAssert(0); return Status; } #ifdef _DEBUG const char *GetCompareHdrs(LDataI *Mail, LDataI *a) { auto s = a->GetStr(FIELD_INTERNET_HEADER); if (!s && Mail) s = Mail->GetStr(FIELD_INTERNET_HEADER); return s; } void CompareTrimWhite(LArray &a) { while (a.Length() > 0 && strchr(WhiteSpace, a[a.Length()-1])) a.Length(a.Length()-1); a.Add(0); } char *CompareChar(char a) { static char buf[4][16]; static int next = 0; char *b = buf[next++]; if (next >= 4) next = 0; if (a < ' ' || ((uint8_t)a) >= 0x80) sprintf_s(b, 16, "0x%2.2X", a); else sprintf_s(b, 16, "'%c'", a); return b; } struct SegMap { LDataI *a, *b; }; bool CompareSegments(LDataI *MailA, LDataI *MailB, LDataI *a, LDataI *b, LStream &p) { const char *s1, *s2; // Check headers s1 = GetCompareHdrs(MailA, a); s2 = GetCompareHdrs(MailB, b); if (s1 && s2 && strcmp(s1, s2)) { p.Print("Seg.Headers(%p,%p),", a, b); return false; } else if ((s1 != 0) ^ (s2 != 0)) { p.Print("Seg.HeaderPtrs,"); return false; } // Check data LAutoStreamI da = a->GetStream(_FL); LAutoStreamI db = b->GetStream(_FL); if (da && db) { LAutoString TransferEncoding(s1?InetGetHeaderField(s1, "Content-Transfer-Encoding"):0); bool IsText = true; if (TransferEncoding && !_stricmp(TransferEncoding, "base64")) IsText = false; if (IsText) { // Text compare LArray bufa, bufb; if (bufa.Length((uint32_t) da->GetSize()) && bufb.Length((uint32_t) db->GetSize())) { da->Read(&bufa[0], bufa.Length()); db->Read(&bufb[0], bufb.Length()); CompareTrimWhite(bufa); CompareTrimWhite(bufb); char *ta = &bufa[0], *tb = &bufb[0]; do { if (*ta != *tb) { p.Print("Seg.TextBody(%s != %s @ %i, %p, %p),", CompareChar(*ta), CompareChar(*tb), ta - &bufa[0], a, b); return false; } ta++; tb++; if (*ta == '\r') ta++; if (*tb == '\r') tb++; } while (*ta && *tb); } else { p.Print("Seg.TextMemAlloc(" LPrintfInt64 "," LPrintfInt64 "),", da->GetSize(), db->GetSize()); return false; } } else { // Binary compare if (da->GetSize() != db->GetSize()) { p.Print("Seg.DataSize(" LPrintfInt64 "," LPrintfInt64 "),", da->GetSize(), db->GetSize()); return false; } else { char bufa[1024], bufb[1024]; for (int64 i=0; iGetSize(); ) { ssize_t ra = da->Read(bufa, sizeof(bufa)); ssize_t rb = db->Read(bufb, sizeof(bufb)); if (ra == rb) { if (memcmp(bufa, bufb, ra)) { p.Print("Seg.DataCmp,"); return false; } i += ra; } else { p.Print("Seg.DataRead,"); return false; } } } } } else if ((da != 0) ^ (db != 0)) { p.Print("Seg.Data(%p[%p,%I64i],%p[%p,%I64i]),", a, da.Get(), da ? da->GetSize() : 0, b, db.Get(), db ? db->GetSize() : 0); return false; } LDataIt ac = a->GetList(FIELD_MIME_SEG); LDataIt bc = b->GetList(FIELD_MIME_SEG); if (ac && bc) { if (ac->Length() != bc->Length()) { p.Print("Seg.ChildLength,"); return false; } else { LArray Map; LDataI *Seg; for (Seg = dynamic_cast(ac->First()); Seg; Seg = dynamic_cast(ac->Next())) { Map.New().a = Seg; } for (Seg = dynamic_cast(bc->First()); Seg; Seg = dynamic_cast(bc->Next())) { auto hdr_b = Seg->GetStr(FIELD_INTERNET_HEADER); bool Matched = false; for (unsigned i=0; iGetStream(_FL); LAutoStreamI db = Seg->GetStream(_FL); if (da && db) { int64 size_a = da->GetSize(); int64 size_b = db->GetSize(); int64 diff = size_a - size_b; if (diff < 0) diff = -diff; if (diff == 0) Exact = true; else if (diff < 8) Close = true; } // What about headers? auto hdr_a = m.a->GetStr(FIELD_INTERNET_HEADER); if (hdr_a && hdr_b) { if (_stricmp(hdr_a, hdr_b)) { Close = false; Exact = false; } } if (Close || Exact) { // Match m.b = Seg; Matched = true; break; } } } if (!Matched) { for (unsigned i=0; iGetStr(FIELD_INTERNET_HEADER); if (hdr_a && hdr_b) { if (!_stricmp(hdr_a, hdr_b)) { // Match m.b = Seg; break; } } } } } } unsigned i; for (i=0; iGetStream(_FL); p.Print("%s%p - %s (%I64i)\r\n", sp, s, ContentType.Get(), Data?Data->GetSize():-1); LDataIt c = s->GetList(FIELD_MIME_SEG); if (c) { for (LDataI *a = dynamic_cast(c->First()); a; a = dynamic_cast(c->Next())) { CompareDumpSegs(p, 0, a, depth+1); } } if (mail) p.Print("\r\n"); } #endif void MailTree::OnItemClick(LTreeItem *Item, LMouse &m) { if (m.Down() && m.IsContextMenu()) { Select(Item); ScribeFolder *t = dynamic_cast(Item); if (t) t->DoContextMenu(m); } } void MailTree::OnItemSelect(LTreeItem *Item) { if (LastWasRoot ^ (int8)Item->IsRoot()) { if (Item->IsRoot()) { if (App->GetItemList()) App->GetItemList()->RemoveAll(); App->SetListPane(new DynamicHtml(App, "title.html")); } else { App->SetListPane(new ThingList(App)); } } if (Things() && !Item->IsRoot()) { ScribeFolder *C = dynamic_cast(Item); if (C) { C->Populate(Things()); App->SetCtrlValue(IDM_THREAD, C->GetThreaded()); App->OnFolderSelect(C); } } else { App->SetCtrlValue(IDM_THREAD, false); } LastWasRoot = Item ? Item->IsRoot() : false; } void MailTree::OnCreateSubDirectory(ScribeFolder *Item) { // setup type list... Store3ItemTypes Type[] = { MAGIC_MAIL, MAGIC_CONTACT, MAGIC_FILTER, MAGIC_CALENDAR, MAGIC_GROUP }; int i=0; for (int n=0; nGetItemType()) { i = n; break; } } // do ui.. bool Enable[] = { Item->CanHaveSubFolders(MAGIC_MAIL), Item->CanHaveSubFolders(MAGIC_CONTACT), Item->CanHaveSubFolders(MAGIC_FILTER), Item->CanHaveSubFolders(MAGIC_CALENDAR), Item->CanHaveSubFolders(MAGIC_GROUP) }; auto Dlg = new CreateSubFolderDlg(this, i, Enable); Dlg->DoModal([this, Dlg, Item, Type](auto dlg, auto code) { if (code && ValidStr(Dlg->SubName) && Dlg->SubType >= 0) { // check the name doesn't conflict.. auto Path = Item->GetPath(); if (Path) { LString s; s.Printf("%s/%s", Path.Get(), Dlg->SubName.Get()); if (App->GetFolder(s)) { LgiMsg(this, LLoadString(IDS_SUBFLD_NAME_CLASH), AppName, MB_OK); Dlg->SubName.Empty(); } } if (Dlg->SubName) { // insert the folder... Item->CreateSubDirectory(Dlg->SubName, Type[Dlg->SubType]); } } delete dlg; }); } void MailTree::OnDelete(ScribeFolder *Item, bool Force) { if (Item) { int FolderType = App->GetFolderType(Item); if (!Force && FolderType >= 0) { char Msg[256]; sprintf_s(Msg, sizeof(Msg), LLoadString(IDS_DELETE_SYS_FOLDER), DefaultFolderNames[FolderType]); LgiMsg(this, Msg, AppName, MB_OK); } else { Item->OnDelete(); } } } void MailTree::OnProperties(ScribeFolder *Item) { if (Item) { Item->OnProperties(); } } LMessage::Result MailTree::OnEvent(LMessage *Msg) { return LTree::OnEvent(Msg); } extern char ScribeFolderObject[]; int MailTree::WillAccept(LDragFormats &Formats, LPoint Pt, int KeyState) { int Status = DROPEFFECT_NONE; LastHit = ItemAtPoint(Pt.x, Pt.y); if (LastHit) { List Accepted; Formats.Supports(ScribeThingList); Formats.Supports(ScribeFolderObject); Formats.SupportsFileDrops(); #if WINNATIVE Formats.Supports(CFSTR_FILEDESCRIPTOR); #endif #ifdef MAC Formats.Supports(LGI_StreamDropFormat); #endif if (Formats.GetSupported().Length()) { SelectDropTarget(LastHit); Status = KeyState & LGI_EF_CTRL ? DROPEFFECT_COPY : DROPEFFECT_MOVE; } } return Status; } static int FolderItemCmp(LTreeItem *a, LTreeItem *b, NativeInt UserData) { ScribeFolder *ta = dynamic_cast(a); ScribeFolder *tb = dynamic_cast(b); if (ta && tb) { int64 IndexA = ta->GetObject()->GetInt(FIELD_FOLDER_INDEX); int64 IndexB = tb->GetObject()->GetInt(FIELD_FOLDER_INDEX); return (int)IndexA - (int)IndexB; } return 0; } int MailTree::OnDrop(LArray &Data, LPoint Pt, int KeyState) { int Status = DROPEFFECT_NONE; ScribeFolder *Leaf = dynamic_cast(LastHit); #if WINNATIVE LString FileDescFmt = CFSTR_FILEDESCRIPTOR; #endif SelectDropTarget(0); if (App) App->SetLastDrop(); for (unsigned idx=0; idxLength(); ScribeFolder *Root = dynamic_cast(ItemAt(0)); for (ssize_t i=0; iLength(); i++) { ScribeFolder *Folder = Fmt->FolderAt(i); if (Folder) { if (Folder == Leaf || Folder == Root || Leaf->GetItemType() == MAGIC_ANY) { // they're dragging onto themselves or // dragging the whole mail tree around or // dragging a folder into the trash // just quit out now... return DROPEFFECT_NONE; } bool CopyOp = (KeyState & LGI_EF_CTRL) != 0; - auto FinishFolderOp = [&](int Res) + auto FinishFolderOp = [this, Folder, Leaf, CopyOp](int Res) { int SystemFolder = App->GetFolderType(Folder); auto OldPath = Folder->GetPath(); Store3Status Status = Store3Error; ScribeFolder *OldParent = Folder->GetFolder(); ScribeFolder *NewParent = NULL; int Index = 0; if (Res == 1) { // Attach next NewParent = Leaf->GetFolder(); if (NewParent) { // Work out the index for (LTreeItem *Item = NewParent->GetChild(); Item && Item!=Leaf; Item=Item->GetNext()) { Index++; } Index++; } } else if (Res == 2) { // Attach child NewParent = Leaf; } else return; if (SystemFolder >= 0 && NewParent->GetObject()->GetStore() != OldParent->GetObject()->GetStore()) { LgiMsg(App, "Can't move system folders to a different mail store.", AppName, MB_OK); return; } if (CopyOp) { // Copy Status = Folder->CopyTo(NewParent, Index); } else { LDataFolderI *fo = Folder->GetFldObj(); if (NewParent == OldParent) { // Re-index only... // int64 OldIndex = fo->GetInt(FIELD_FOLDER_INDEX); if (fo->SetInt(FIELD_FOLDER_INDEX, Index)) { // Change the UI to match... NewParent->Sort(FolderItemCmp); Status = Store3Success; } } else { // Move Status = Folder->SetFolder(NewParent, Index); if (Status) { NewParent->Sort(FolderItemCmp); } } } if (Status == Store3Success && SystemFolder >= 0 && !CopyOp) { // Update the system path location if it's changed... auto NewPath = Folder->GetPath(); if (OldPath && NewPath && strcmp(OldPath, NewPath) != 0) { LVariant v; v = NewPath; LString SysFolderName; SysFolderName.Printf("Folder-%i", SystemFolder); App->GetOptions()->SetValue(SysFolderName, v); } } }; int Res = 2; if (Leaf->GetFolder() != NULL) { auto Dlg = new LAlert( this, LLoadString(CopyOp ? IDS_COPY_FOLDER : IDS_MOVE_FOLDER), LLoadString(IDS_MOVE_ATTACH), LLoadString(IDS_NEXT_FOLDER), LLoadString(IDS_SUB_FOLDER), LLoadString(IDS_CANCEL)); Dlg->DoModal([this, Dlg, FinishFolderOp](auto dlg, auto Res) { if (Res > 0) FinishFolderOp(Res); delete dlg; }); } else FinishFolderOp(Res); } } } else if (ScribeClipboardFmt::IsThing(v.Value.Binary.Data, v.Value.Binary.Length)) { ScribeClipboardFmt *Fmt = (ScribeClipboardFmt*) v.Value.Binary.Data; LDataStoreI::StoreTrans Trans = Leaf->GetObject()->GetStore()->StartTransaction(); bool CopyOnly = (KeyState & LGI_EF_CTRL) != 0; Count = Fmt->Length(); LArray Items; for (ssize_t i=0; iThingAt(i); if (Thg) Items.Add(Thg); } if (Items.Length()) { LArray ItemStatus; - Leaf->MoveTo(Items, CopyOnly, &ItemStatus); + Leaf->MoveTo(Items, CopyOnly, NULL); + // Fixme: Impl MoveTo Callback for (auto s: ItemStatus) if (s == Store3Error) Errors++; } } else LAssert(!"Unknown drop format."); App->Update(); if (Errors) { LgiMsg(this, LLoadString(IDS_MOVE_ERROR), AppName, MB_OK, Errors, Count); } else { Status = DROPEFFECT_COPY; } // We don't need to process any other data types... we're done. break; } else if (dd.IsFileDrop()) { if (dd.Data.Length() > 0) { LDropFiles Files(dd.Data[0]); Leaf->OnReceiveFiles(Files); Status = DROPEFFECT_COPY; } } #if WINNATIVE else if (_stricmp(dd.Format, FileDescFmt) == 0) { if (dd.Data.Length() == 0 || dd.Data[0].Type != GV_BINARY) continue; // Get file list... LString::Array Files; FILEGROUPDESCRIPTOR *FileGroup = (FILEGROUPDESCRIPTOR*) dd.Data[0].Value.Binary.Data; if (OnDropFileGroupDescriptor(FileGroup, Files)) { // Process dropped files LArray FileLst; for (auto &f : Files) FileLst.Add(f.Get()); Leaf->OnReceiveFiles(FileLst); // Clean up for (auto f : Files) { FileDev->Delete(f, false); } } else if (DataObject) { for (int i=0; true; i++) { FORMATETC Format; Format.cfFormat = RegisterClipboardFormat(CFSTR_FILECONTENTS); Format.dwAspect = DVASPECT_CONTENT; Format.lindex = i; Format.tymed = TYMED_ISTORAGE; Format.ptd = 0; STGMEDIUM Medium; ZeroObj(Medium); HRESULT res = DataObject->GetData(&Format, &Medium); if (SUCCEEDED(res)) { if (Medium.tymed == TYMED_ISTORAGE) { OutlookIStorage Dec(App, Medium.pstg, false); int Type = Dec.GetType(); if (Type == Leaf->GetItemType()) { switch (Type) { case MAGIC_MAIL: { Mail *m = Dec.GetMail(); if (m) { m->App = App; m->Save(Leaf); Status = DROPEFFECT_COPY; } break; } case MAGIC_CONTACT: { Contact *c = Dec.GetContact(); if (c) { c->App = App; c->Save(Leaf); Status = DROPEFFECT_COPY; } break; } } } } ReleaseStgMedium(&Medium); } else break; } } } #endif } return Status; } diff --git a/Code/ScribeMail.cpp b/Code/ScribeMail.cpp --- a/Code/ScribeMail.cpp +++ b/Code/ScribeMail.cpp @@ -1,10254 +1,10255 @@ /* ** 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); + OutBox->MoveTo(Items, false); } } } 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) + Select->DoModal([Select, EditCtrl](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); + Sent->MoveTo(Items, false); } } } 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(); 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].Get(), 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); - } + Spam->MoveTo(Items, false, [View](auto result, auto status) + { + if (!result) + 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/ScribePageSetup.cpp b/Code/ScribePageSetup.cpp --- a/Code/ScribePageSetup.cpp +++ b/Code/ScribePageSetup.cpp @@ -1,225 +1,225 @@ #include #include #include "Scribe.h" #include "ScribePageSetup.h" #include "resdefs.h" //////////////////////////////////////////////////////////////// class BorderEdit : public LLayout, public ResObject { friend class ScribePageSetup; double x1, y1, x2, y2; double PageX, PageY; double Scale; public: BorderEdit() : ResObject(Res_Custom) { x1 = y1 = x2 = y2 = 1.0; PageX = PageDefaultX; // A4 is the default PageY = PageDefaultY; Scale = 1.0; // scaling.. Sunken(true); } void SetBorders(double X1, double Y1, double X2, double Y2) { x1 = limit(X1, 0.0, PageX/2); y1 = limit(Y1, 0.0, PageY/2); x2 = limit(X2, 0.0, PageX/2); y2 = limit(Y2, 0.0, PageY/2); Invalidate(); } void OnPaint(LSurface *pDC) { // calculate scaling // double Aspect = PageY / PageX; Scale = ((double)Y()) * 0.8 / PageY; // paint background pDC->Colour(L_LOW); pDC->Rectangle(); // paint doc... int Cx = (int) (X()/2); int Cy = (int) (Y()/2); int x = (int) (Scale * PageX); int y = (int) (Scale * PageY); LRect r(Cx - (x/2), Cy - (y/2), Cx + (x/2), Cy + (y/2)); pDC->Colour(L_BLACK); pDC->Box(&r); r.Inset(1, 1); pDC->Colour(L_WHITE); pDC->Rectangle(&r); // paint borders.. pDC->Colour(L_LTGREY); int n = (int) (r.x1 + (Scale * x1)); // left pDC->Line(n, r.y1, n, r.y2); n = (int) (r.y1 + (Scale * y1)); // top pDC->Line(r.x1, n, r.x2, n); n = (int) (r.x2 - (Scale * x2)); // right pDC->Line(n, r.y1, n, r.y2); n = (int) (r.y2 - (Scale * y2)); // bottom pDC->Line(r.x1, n, r.x2, n); // paint dummy lines r.x1 += (int) (Scale * x1); r.y1 += (int) (Scale * y1); r.x2 -= (int) (Scale * x2); r.y2 -= (int) (Scale * y2); r.Inset(2, 2); for (int i=r.y1; iRectangle(r.x1, i, (i%4==3) ? r.x1 + (r.X()*2/3) : r.x2, i+2); } } bool OnLayout(LViewLayoutInfo &Inf) { if (!Inf.Width.Max) { Inf.Width.Max = 10000; Inf.Width.Min = 100; } else if (!Inf.Height.Max) { Inf.Height.Max = 10000; Inf.Height.Min = 100; } else return false; return true; } }; class BorderEditFactory : public LViewFactory { LView *NewView(const char *Class, LRect *Pos, const char *Text) { if (!_stricmp(Class, "BorderEdit")) return new BorderEdit; return NULL; } public: } BorderEditFact; //////////////////////////////////////////////////////////////// ScribePageSetup::ScribePageSetup(LView *parent, LOptionsFile *options) { Options = options; SetParent(parent); if (LoadFromResource(IDD_PAGE_SETUP)) { MoveToCenter(); SetCtrlEnabled(IDC_FONT, false); Serialize(false); // update the ctrl LViewI *w = FindControl(IDC_LEFT); LNotification note(LNotifyValueChanged); if (w) OnNotify(w, note); } } void ScribePageSetup::Serialize(bool Write) { if (Options) { Font.Serialize(Options, OPT_PrintFont, Write); if (Write) { #define SaveBorder(ctrl, opt) \ { LVariant s; \ s = GetCtrlName(ctrl); \ if (s.Str()) \ Options->SetValue(opt, s); \ } SaveBorder(IDC_LEFT, OPT_MarginX1); SaveBorder(IDC_TOP, OPT_MarginY1); SaveBorder(IDC_RIGHT, OPT_MarginX2); SaveBorder(IDC_BOTTOM, OPT_MarginY2); } else { char s[128]; #define LoadBorder(ctrl, opt) \ { LVariant d; \ if (Options->GetValue(opt, d)) \ { sprintf_s(s, sizeof(s), "%.2f", d.CastDouble()); \ SetCtrlName(ctrl, s); } \ else SetCtrlName(ctrl, "1.00"); } LoadBorder(IDC_LEFT, OPT_MarginX1); LoadBorder(IDC_TOP, OPT_MarginY1); LoadBorder(IDC_RIGHT, OPT_MarginX2); LoadBorder(IDC_BOTTOM, OPT_MarginY2); if (Font.GetDescription(s, sizeof(s))) { SetCtrlName(IDC_FONT, s); } } } } int ScribePageSetup::OnNotify(LViewI *Ctrl, LNotification n) { switch (Ctrl->GetId()) { case IDC_LEFT: case IDC_TOP: case IDC_RIGHT: case IDC_BOTTOM: { BorderEdit *Ctrl; if (GetViewById(IDC_PREVIEW, Ctrl)) { auto x1 = GetCtrlName(IDC_LEFT); auto y1 = GetCtrlName(IDC_TOP); auto x2 = GetCtrlName(IDC_RIGHT); auto y2 = GetCtrlName(IDC_BOTTOM); if (x1 && x2 && y1 && y2) { Ctrl->SetBorders(atof(x1), atof(y1), atof(x2), atof(y2)); } } break; } case IDC_BROWSE_FONT: { - Font.DoUI(this, [&](auto fontType) + Font.DoUI(this, [this](auto fontType) { char s[256]; Font.GetDescription(s, sizeof(s)); SetCtrlName(IDC_FONT, s); }); break; } case IDOK: { Serialize(true); // fall thru } case IDCANCEL: { EndModal(Ctrl->GetId()); break; } } return 0; } diff --git a/Code/ScribePassword.cpp b/Code/ScribePassword.cpp --- a/Code/ScribePassword.cpp +++ b/Code/ScribePassword.cpp @@ -1,117 +1,117 @@ #include "Scribe.h" class ScribePasswordPrivate { public: LView *Dlg; LOptionsFile *Props; const char *Opt; int CtrlEnable; int CtrlPwd; int CtrlConfirm; }; ScribePassword::ScribePassword(LOptionsFile *p, const char *opt, int check, int pwd, int confirm) { d = new ScribePasswordPrivate; d->Props = p; d->Opt = opt; d->CtrlEnable = check; d->CtrlPwd = pwd; d->CtrlConfirm = confirm; d->Dlg = 0; } ScribePassword::~ScribePassword() { DeleteObj(d); } bool ScribePassword::IsOk() { bool Status = #ifndef __llvm__ this != 0 && #endif d != 0 && d->Dlg != 0 && d->Opt != 0 && d->Props != 0; LAssert(Status); return Status; } bool ScribePassword::Load(LView *dlg) { bool Status = false; d->Dlg = dlg; if (IsOk()) { - GPassword p; + LPassword p; if (p.Serialize(d->Props, d->Opt, false)) { d->Dlg->SetCtrlValue(d->CtrlEnable, true); } else { d->Dlg->SetCtrlEnabled(d->CtrlPwd, false); d->Dlg->SetCtrlEnabled(d->CtrlConfirm, false); } Status = true; } return Status; } bool ScribePassword::Save() { bool Status = false; if (IsOk()) { if (d->Dlg->GetCtrlValue(d->CtrlEnable)) { auto Psw = d->Dlg->GetCtrlName(d->CtrlPwd); auto Confirm = d->Dlg->GetCtrlName(d->CtrlConfirm); if (ValidStr(Psw) && ValidStr(Confirm)) { if (strcmp(Psw, Confirm) == 0) { - GPassword p; + LPassword p; p.Set(Psw); if (p.Serialize(d->Props, d->Opt, true)) { Status = true; } } else { LgiMsg(d->Dlg, "Passwords don't match.", AppName); } } else if (ValidStr(Psw) || ValidStr(Confirm)) { LgiMsg(d->Dlg, "Both the password and the confirmation need to be entered.", AppName); } else { // Unchanged Status = true; } } else { d->Props->DeleteValue(d->Opt); Status = true; } } return Status; } void ScribePassword::OnNotify(LViewI *c, LNotification &n) { if (c->GetId() == d->CtrlEnable && IsOk()) { bool On = c->Value() != 0; d->Dlg->SetCtrlEnabled(d->CtrlPwd, On); d->Dlg->SetCtrlEnabled(d->CtrlConfirm, On); } } diff --git a/Code/ScribeSendReceive.cpp b/Code/ScribeSendReceive.cpp --- a/Code/ScribeSendReceive.cpp +++ b/Code/ScribeSendReceive.cpp @@ -1,2848 +1,2847 @@ /* ** 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; + LPassword 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; + LPassword 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 = [&]() + auto StartThread = [this]() { 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; + LPassword 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, StartThread](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 Accountlet::GetPassword(LPassword *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) +void Accountlet::SetPassword(LPassword *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; + LPassword 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()) { 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; + LPassword 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/ScribeStatusPanel.cpp b/Code/ScribeStatusPanel.cpp --- a/Code/ScribeStatusPanel.cpp +++ b/Code/ScribeStatusPanel.cpp @@ -1,881 +1,883 @@ #include "Scribe.h" #include "ScribePrivate.h" #include "ScribeStatusPanel.h" #include "resdefs.h" #include "lgi/common/ClipBoard.h" #include "lgi/common/TableLayout.h" #include "lgi/common/LgiRes.h" #include "lgi/common/TextView4.h" #include "lgi/common/TextLog.h" #include "lgi/common/TabView.h" //////////////////////////////////////////////////////////////////////////// #define STATUS_BASE 22 #define UP_ARROW 4 #define DOWN_ARROW 5 #define BLACK_ARROW 6 #define FTP_SIZE 22 #define IDM_SEND 100 #define IDM_RECEIVE 101 #define IDM_PREVIEW 102 #define IDM_CONFIG 103 const char *AccountStatusTxt[] = { "Idle", "Connected", "Waiting", "Error" }; //////////////////////////////////////////////////////////////////////////// typedef LThreadSafeTextView LAccountLogParent; class LAccountLog : public LAccountLogParent { LArray Rgb; LString sEmpty; public: LogEntry *Prev; LAccountLog() : LAccountLogParent(-1) { Prev = NULL; SetPourLargest(true); SetWrapType(TEXTED_WRAP_NONE); sEmpty.Printf("(%s)", LLoadString(IDS_EMPTY)); LFont *f = new LFont; if (f) { *f = *LSysFont; f->PointSize(f->PointSize() - 1); SetFont(f, true); } } void Empty() { Name(NULL); Rgb.Length(0); } void AddText(char16 *Txt, size_t Chars, LColour &c) { for (size_t i=0; i= Rgb.Length()) break; tl->c.c32(Rgb[i]); i++; } } void OnPaint(LSurface *pDC) { if (LAccountLogParent::Length() == 0) { LColour f(192,192,192), b(L_WORKSPACE); auto Fnt = GetFont(); LDisplayString ds(Fnt, sEmpty); auto c = GetClient(); Fnt->Transparent(false); Fnt->Colour(f, b); ds.Draw(pDC, 4, 4, &c); } else LAccountLogParent::OnPaint(pDC); } }; class LAccountLogFactory : public LViewFactory { LView *NewView ( /// The name of the class to create const char *Class, /// The initial position of the view LRect *Pos, /// The initial text of the view const char *Text ) { if (!_stricmp(Class, "LAccountLog")) return new LAccountLog(); return NULL; } } AccountLogFactory; //////////////////////////////////////////////////////////////////////////// AccountStatusItem::AccountStatusItem(AccountStatusPanel *panel, ScribeAccount *account, LImageList *imglst) { Buf[0] = 0; Panel = panel; ImgLst = imglst; Account = account; Account->Views.Add(this); State = STATUS_OFFLINE; SetImage(ICON_UNSENT_MAIL); } AccountStatusItem::~AccountStatusItem() { LAssert(Account->Views.HasItem(this)); Account->Views.Delete(this); } const char *AccountStatusItem::GetText(int Col) { char *Status = 0; if (Account) { if (Col == 2) { LVariant v = Account->Receive.Name(); if (!v.Str()) { v = Account->Receive.Server(); } if (!v.Str()) { v = Account->Send.Server(); } static char Buf[64]; if (v.Str()) { strcpy_s(Buf, sizeof(Buf), v.Str()); Status = Buf; } } else if (Col == 3) { return Buf; } } return Status; } int AccountStatusItem::Compare(LListItem *To, ssize_t Field) { int ASort = Account->Identity.Sort(); AccountStatusItem *b = dynamic_cast(To); if (!b) return 0; int BSort = b->Account->Identity.Sort(); return ASort - BSort; } void AccountStatusItem::OnPaintColumn(LItem::ItemPaintCtx &Ctx, int i, LItemColumn *c) { LListItem::OnPaintColumn(Ctx, i, c); if (ImgLst && Account) { LRect *Bounds = ImgLst->GetBounds(); if (Bounds) { int Icon = -1; if (i == 0) { // Send Status if (Account->Send.IsConfigured()) Icon = STATUS_BASE + (Account->Send.IsOnline() ? STATUS_ONLINE : STATUS_OFFLINE); } else if (i == 1) { // Receive Status if (Account->Receive.IsConfigured()) Icon = STATUS_BASE + Panel->AccountStatus(&Account->Receive); } if (Icon >= 0) { Bounds += Icon; int x = Ctx.x1 + ((Ctx.X()-Bounds->X())/2) - Bounds->x1; int y = Ctx.y1 + ((Ctx.Y()-Bounds->Y())/2) - Bounds->y1; LColour Back(Ctx.Back); ImgLst->Draw(Ctx.pDC, x, y, Icon, Back); } } } } void AccountStatusItem::OnPulse() { if (Account) { Buf[0] = 0; Account->OnPulse(Buf, sizeof(Buf)); Update(); } } void AccountStatusItem::OnMouseClick(LMouse &m) { if (!Account) return; if (m.IsContextMenu()) { LSubMenu RClick; LArray Acc; List Sel; int RecAcc = 0; int SendAcc = 0; if (GetList()->GetSelection(Sel)) { for (auto i: Sel) { auto *si = dynamic_cast(i); Acc.Add(si->Account); if (!si->Account->Receive.Disabled()) { RecAcc += si->Account->Receive.Server().Str() ? 1 : 0; SendAcc += si->Account->Send.Server().Str() ? 1 : 0; } } } RClick.AppendItem(LString(LLoadString(IDS_SEND)) + " " + Account->Send.Server().Str(), IDM_SEND, SendAcc != 0); RClick.AppendItem(LString(LLoadString(IDS_RECEIVE)) + " " + Account->Receive.Server().Str(), IDM_RECEIVE, RecAcc != 0); RClick.AppendItem(LLoadString(IDS_PREVIEW), IDM_PREVIEW, RecAcc != 0); RClick.AppendSeparator(); RClick.AppendItem(LLoadString(IDS_CONFIGURE), IDM_CONFIG, Acc.Length() == 1); switch (RClick.Float(GetList(), m)) { case IDM_SEND: { if (!Account->Send.IsOnline()) { auto Idx = Account->GetIndex(); Panel->App->Send(Idx); } break; } case IDM_RECEIVE: { if (!Account->Receive.IsOnline()) { Account->Receive.Connect(0, false); } break; } case IDM_PREVIEW: { OpenPopView(Panel->App, Acc); break; } case IDM_CONFIG: { - Account->GetApp()->GetAccountSettingsAccess(GetList(), ScribeReadAccess, [&](auto status) - { - if (status) - Account->InitUI(Parent, 0, NULL); - }); + Account->GetApp()->GetAccountSettingsAccess(GetList(), + ScribeReadAccess, + [this](auto status) + { + if (status) + Account->InitUI(Parent, 0, NULL); + }); break; } } } else if (m.Left() && m.Double()) { // receive if (Account->Receive.IsConfigured()) { Panel->App->Receive(Account->GetIndex()); } else if (Account->Send.IsConfigured()) { Panel->App->Send(Account->GetIndex()); } } } //////////////////////////////////////////////////////////////////////////// #define OPT_StatusOpen "ScribeUI.StatusOpen" AccountStatusPanel::AccountStatusPanel(ScribeWnd *app, LImageList *imglst) : ScribePanel(app, LLoadString(IDS_STATUS), 20, false) { App = app; Accounts = (App) ? App->GetAccounts() : 0; ImgLst = imglst; PrevAccounts = 0; Current = 0; Lst = 0; Total = 0; Sub = 0; AccountTbl = 0; ProgressTbl = 0; CurStatusItem = NULL; Alignment(GV_EDGE_BOTTOM); LVariant Op = false; App->GetOptions()->GetValue(OPT_StatusOpen, Op); if (Op.CastInt32()) { Open(Op.CastInt32() != 0); } LRect *Bounds = ImgLst ? ImgLst->GetBounds() : 0; LAutoString n; LRect p; if (Bounds && LoadFromResource(IDD_STATUS, this, &p, &n)) { int MaxY = 0; for (auto c: Children) { MaxY = MAX(MaxY, c->GetPos().y2); } SetOpenSize(MaxY + 10); GetViewById(IDC_ACCOUNT_TBL, AccountTbl); GetViewById(IDC_PROGRESS_TBL, ProgressTbl); GetViewById(IDC_OP_PROG, Total); GetViewById(IDC_EMAIL_PROG, Sub); if (GetViewById(IDC_ACC_LOG, Log)) { Log->SetPourChildren(true); } if (GetViewById(IDC_ACC_LIST, Lst)) { Lst->SetImageList(ImgLst, false); int x = MAX(Bounds[STATUS_BASE + UP_ARROW].X(), Bounds[STATUS_BASE + STATUS_OFFLINE].X()); LItemColumn *c = Lst->AddColumn("Send", x + 6); if (c) { c->Image(STATUS_BASE + UP_ARROW); c->Resizable(false); } x = MAX(Bounds[STATUS_BASE + DOWN_ARROW].X(), Bounds[STATUS_BASE + STATUS_OFFLINE].X()); c = Lst->AddColumn("Receive", x + 6); if (c) { c->Image(STATUS_BASE + DOWN_ARROW); c->Resizable(false); } c = Lst->AddColumn(LLoadString(IDS_SERVER), 120); c = Lst->AddColumn(LLoadString(IDS_TIME), 50); } } }; AccountStatusPanel::~AccountStatusPanel() { LVariant v; auto Opts = App->GetOptions(); Opts->SetValue(OPT_StatusOpen, v = Open()); } void AccountStatusPanel::OnPosChange() { LLayoutRect c(this); if (Open() && c.Valid()) { c.x1 += 20; int FontHt = LSysFont->GetHeight(); c.Left(AccountTbl, 170 + FontHt * 6); c.Left(ProgressTbl, 200 + FontHt * 12); c.Remaining(Log); } } bool AccountStatusPanel::_Lock() { return App->Lock(_FL); } void AccountStatusPanel::_Unlock() { App->Unlock(); } int AccountStatusPanel::OnNotify(LViewI *Ctrl, LNotification n) { switch (Ctrl->GetId()) { case IDC_STOP: { // stop this client now static bool Stopping = false; if (!Stopping) { Stopping = true; if (_Lock()) { Accountlet *AccLet = 0; if (Current) { if (Current->Receive.IsOnline()) { AccLet = &Current->Receive; } else if (Current->Send.IsOnline()) { AccLet = &Current->Send; } } _Unlock(); if (AccLet) { AccLet->Disconnect(); } } Stopping = false; } else { printf("Recursion lockout.\n"); } break; } } return 0; } void AccountStatusPanel::OnAccountSelect(AccountStatusItem *Item) { Current = (Item) ? Item->Account : 0; if (Total && Sub && _Lock()) { Accountlet *AccLet = 0; if (Current) { bool IsSending = Current->Send.IsOnline(); bool IsReceiving = Current->Receive.IsOnline(); if ((IsSending ^ IsReceiving) == 0) { // Accounts are either both online or both offline if (Current->Send.GetLastOnline() > Current->Receive.GetLastOnline()) { AccLet = &Current->Send; } else { AccLet = &Current->Receive; } } else { // Only one is online if (IsSending) { AccLet = &Current->Send; } else if (IsReceiving) { AccLet = &Current->Receive; } } } bool IsCur = AccLet ? AccLet->IsOnline() : false; Total->Enabled(IsCur); Sub->Enabled(IsCur); SetCtrlEnabled(IDC_STATUS_TXT, IsCur); if (Accounts && Item) { int Status = AccountStatus(AccLet); // update the list item if needed if (Item->State != Status) { Item->Update(); Item->State = Status; } // get ptrs to the progress objects const char *OpDesc = AccLet ? AccLet->GetStateName() : 0; SetCtrlName(IDC_STATUS_TXT, OpDesc); } else { SetCtrlName(IDC_STATUS_TXT, LLoadString(IDS_SELECT_ACCOUNT)); } if (AccLet) { // Connection char Str[256]; if (AccLet->Group.Range) { Total->SetRange(AccLet->Group.Range); Total->Value(AccLet->Group.Value); int Ch = sprintf_s(Str, sizeof(Str), LLoadString(IDS_EMAIL_PROGRESS), AccLet->Group.Value, AccLet->Group.Range); if (AccLet->Group.Start) { double Sec = (double)(LCurrentTime() - AccLet->Group.Start)/1000.0; if (Sec > 0.0) { double Rate = (double)AccLet->Group.Value / Sec; sprintf_s(Str+Ch, sizeof(Str)-Ch, " (%.1f/s)", Rate); } } SetCtrlName(IDC_OP_TXT, Str); } else { SetCtrlName(IDC_OP_TXT, 0); Total->Value(0); } // Mail if (AccLet->Item.Range) { Sub->SetRange(AccLet->Item.Range); Sub->Value(AccLet->Item.Value); } else { Sub->Value(0); } if (AccLet->Item.Start) { double Rate = 0.0; uint64 Period = LCurrentTime() - AccLet->Item.Start; double Sec = (double)Period / 1000.0; if (Period > 0) Rate = (double)AccLet->Item.Value / Sec; if (AccLet->Item.Range) { char sVal[32], sRange[32], sRate[32]; LFormatSize(sVal, sizeof(sVal), AccLet->Item.Value); LFormatSize(sRange, sizeof(sRange), AccLet->Item.Range); LFormatSize(sRate, sizeof(sRate), (uint64)Rate); sprintf_s(Str, sizeof(Str), "%s of %s (%s/second)", sVal, sRange, sRate); } else { char sRate[32]; LFormatSize(sRate, sizeof(sRate), (uint64)Rate); sprintf_s(Str, sizeof(Str), "%s/second", sRate); } SetCtrlName(IDC_EMAIL_TXT, Str); } else { SetCtrlName(IDC_EMAIL_TXT, 0); } } else { SetCtrlName(IDC_OP_TXT, 0); SetCtrlValue(IDC_OP_PROG, 0); SetCtrlName(IDC_EMAIL_TXT, 0); SetCtrlValue(IDC_EMAIL_PROG, 0); Total->Value(0); Sub->Value(0); } if (ProgressTbl) ProgressTbl->InvalidateLayout(); SetCtrlEnabled(IDC_STOP, AccLet && AccLet->IsOnline()); // do log list if (Log) { int Ctrls[2] = {IDC_SEND_LOG, IDC_RECEIVE_LOG}; if (CurStatusItem != Item) { CurStatusItem = Item; for (int i=0; iGetViewById(Ctrls[i], al)) al->Empty(); } } if (Current) { Accountlet *Lets[2] = { &Current->Send, &Current->Receive }; for (int i=0; iGetViewById(Ctrls[i], al)) { auto Priv = Lets[i]->Lock(_FL); if (Priv) { auto &l = Priv->d->Log; if (l.Length()) { if (!al->Prev || al->Prev != l[0]) { // Changed content al->Empty(); al->Prev = l[0]; } auto StartTs = LCurrentTime(); size_t pos = 0; for (unsigned i=0; iTxt.Length(); size_t ch = al->LAccountLogParent::Length(); if (end > ch) { ssize_t offset = 0; size_t len = e->Txt.Length(); if (al->LAccountLogParent::Length() > pos) { offset = al->LAccountLogParent::Length() - pos; len -= offset; } LColour c = e->GetColour(); al->AddText(e->Txt.AddressOf(offset), len, c); } pos += e->Txt.Length(); auto CurTs = LCurrentTime(); if (CurTs - StartTs >= 1000) break; } auto Taken = LCurrentTime() - StartTs; if (Taken > 200) { al->Empty(); // Delete the first 1/3 of the log... LRange r(0, (ssize_t)(l.Length() * 0.33)); if (r.Len == 0) r.Len++; LgiTrace("%s:%i - Log processing blocking (" LPrintfInt64 "ms), removing " LPrintfSizeT " oldest items.\n", _FL, Taken, r.Len); l.DeleteRange(r); al->Prev = l[0]; } } else if (!al->Length()) { al->Empty(); } } } } } } _Unlock(); } } int AccountStatusPanel::AccountStatus(Accountlet *a) { return a ? a->GetStatusIcon() : STATUS_ERROR; } int AccountStatusPanel::CalcWidth() { int BaseX = LPanel::CalcWidth(); LRect *Bounds = ImgLst ? ImgLst->GetBounds() : 0; if (Bounds) { ScribeAccount *Send = App->GetSendAccount(); int UpArrow = STATUS_BASE + UP_ARROW; int SendIcon = STATUS_BASE + (Send ? AccountStatus(&Send->Send) : 0); int DownArrow = STATUS_BASE + DOWN_ARROW; BaseX += Bounds[UpArrow].X() + 2; BaseX += Bounds[SendIcon].X() + 2; BaseX += 8; BaseX += Bounds[DownArrow].X() + 2; for (size_t i=0; iLength(); i++) { ScribeAccount *Acc = Accounts->ItemAt(i); if (Acc && Acc->Receive.IsConfigured()) { int ReceiveIcon = STATUS_BASE + AccountStatus(&Acc->Receive); BaseX += Bounds[ReceiveIcon].X() + 2; } } } return BaseX; } void AccountStatusPanel::OnPaint(LSurface *pDC) { #ifdef __GTK_H__ LDoubleBuffer Buf(pDC); #endif LPanel::OnPaint(pDC); if (!IsOpen && ImgLst && Accounts) { int x = LPanel::CalcWidth() - 8, y = 3; auto Accs = Accounts->Length(); ScribeAccount *Send = App->GetSendAccount(); LColour Background(L_MED); LRect *Bounds = ImgLst->GetBounds(); if (Bounds) { // SMTP int UpArrow = STATUS_BASE + UP_ARROW; ImgLst->Draw(pDC, x, y, UpArrow, Background); // arrow x += Bounds[UpArrow].X() + 2; if (Send) { int SendIcon = STATUS_BASE + AccountStatus(&Send->Send); ImgLst->Draw(pDC, x, y, SendIcon, Background); x += Bounds[SendIcon].X() + 2; } // break x += 8; // Receive accounts int DownArrow = STATUS_BASE + DOWN_ARROW; ImgLst->Draw(pDC, x, y, DownArrow, Background); // arrow x += Bounds[DownArrow].X() + 2; for (size_t i=0; iItemAt(i); if (Acc && Acc->Receive.IsConfigured()) { int ReceiveIcon = STATUS_BASE + AccountStatus(&Acc->Receive); ImgLst->Draw(pDC, x, y, ReceiveIcon, Background); x += Bounds[ReceiveIcon].X() + 2; } } } } } void AccountStatusPanel::OnPulse() { if (Accounts && PrevAccounts != Accounts->Length()) { OnAccountListChange(); } if (!IsOpen) { // Invalidate(); } else if (Lst) { OnAccountSelect(dynamic_cast(Lst->GetSelected())); } if (Lst) { List All; Lst->GetAll(All); for (auto a: All) { a->OnPulse(); } } } void AccountStatusPanel::Empty() { OnAccountSelect(0); if (Lst) { Lst->Empty(); } } int AccountItemCmp(LListItem *a, LListItem *b, NativeInt Data) { return a->Compare(b); } void AccountStatusPanel::OnAccountListChange() { if (Lst) { Lst->Empty(); if (Accounts) { PrevAccounts = Accounts->Length(); RePour(); for (auto a: *Accounts) Lst->Insert(new AccountStatusItem(this, a, ImgLst)); Lst->Sort(AccountItemCmp); auto All = Lst->begin(); if (*All && !Lst->GetSelected()) { (*All)->Select(true); } } } Invalidate(); } LXmlTag *AccountStatusPanel::GetOptions() { return 0; } void AccountStatusPanel::SetDataRate(int Percent) { } diff --git a/Code/ScribeThing.cpp b/Code/ScribeThing.cpp --- a/Code/ScribeThing.cpp +++ b/Code/ScribeThing.cpp @@ -1,925 +1,929 @@ #include "lgi/common/Lgi.h" #include "Scribe.h" #include "../Resources/resdefs.h" #include "Store3Imap/ScribeImap.h" #include "lgi/common/LgiRes.h" #include "lgi/common/FileSelect.h" ///////////////////////////////////////////////////////////////////////////////// ThingType::ThingType() { } ThingType::~ThingType() { if (Dirty) { if (DirtyThings.HasItem(this)) { LAssert(!"Should not be deleting something in the dirty list...?"); DirtyThings.Delete(this); } } } void ThingType::WhenLoaded(const char *file, int line, std::function Callback, int index) { if (!Callback) { LAssert(!"No callback."); return; } if (!Loaded && GetObject()) { // Lets just check the state of the object first... auto i = GetObject()->GetInt(FIELD_LOADED); if (i == Store3Loaded) { // This is the default for mail3 for instance... Loaded = true; } } if (Loaded) { Callback(); } else { ThingEventInfo *cb = new ThingEventInfo; cb->File = file; cb->Line = line; cb->Callback = Callback; if (index < 0) OnLoadCallbacks.Add(cb); else OnLoadCallbacks.AddAt(index, cb); } } bool ThingType::IsLoaded(int Set) { if (Set >= 0) { if (!Loaded && Set > 0) { Loaded = true; for (auto cb: OnLoadCallbacks) { // LgiTrace("OnLoadCallbacks %s:%i\n", cb.File, cb.Line); cb->Callback(); } OnLoadCallbacks.DeleteObjects(); } Loaded = Set > 0; } return Loaded; } bool ThingType::SetDirty(bool b) { bool Status = false; if (WillDirty) { Thing *t = dynamic_cast(this); if (t && t->GetObject() && t->GetObject()->GetInt(FIELD_STORE_TYPE) == Store3Imap) { // IMAP email need to be explicitly saved when the message // is fully constructed. return true; } if (Dirty == b) { if (Dirty) LAssert(Thing::DirtyThings.HasItem(this)); else LAssert(!Thing::DirtyThings.HasItem(this)); } else { if (b) { Dirty = true; if (!Thing::DirtyThings.HasItem(this)) Thing::DirtyThings.Add(this); } else { Dirty = false; Thing::DirtyThings.Delete(this); } } } return Status; } ///////////////////////////////////////////////////////////////////////////////// LArray ThingType::DirtyThings; Thing::Thing(ScribeWnd *app, LDataI *object) { _UserPtr = this; App = app; IncRef(); // Someone always starts with owning this object. SetObject(object, false, _FL); } Thing::~Thing() { if (GetUI()) { LAssert(!"Really, should we still be linked to a UI here?"); } DirtyThings.Delete(this); DeleteObj(Data); SetParentFolder(NULL); auto o = GetObject(); if (o) { SetObject(NULL, true, _FL); DeleteObj(o); } } LDataI *Thing::DefaultObject(LDataI *arg) { LAssert(App != NULL); if (arg) { SetObject(arg, false, _FL); } else if (!GetObject() && App && App->GetDefaultMailStore()) { LMailStore *Ms = App->GetDefaultMailStore(); if (Ms) SetObject(Ms->Store->Create(Type()), false, _FL); } return GetObject(); } bool Thing::OnKey(LKey &k) { #ifndef WINDOWS // This is being done by the VK_APPS key on windows... if (k.IsContextMenu()) { if (k.Down()) { LMouse m; m.x = 5; m.y = 5; m.ViewCoords = true; m.Target = GetList(); DoContextMenu(m); } return true; } #endif return false; } void Thing::SetParentFolder(ScribeFolder *f) { if (GetFolder() == f) return; if (_ParentFolder) { if (!_ParentFolder->Items.HasItem(this)) { LAssert(!"_ParentFolder->Items incorrect."); } _ParentFolder->Items.Delete(this); } _ParentFolder = f; if (_ParentFolder) { LAssert(!_ParentFolder->Items.HasItem(this)); _ParentFolder->Items.Insert(this); } } Store3Status Thing::SetFolder(ScribeFolder *New, int Param) { Store3Status Moved = Store3Error; if (New) { ScribeFolder *Old = GetFolder(); if (Old) { if (Old->GetObject() && New->GetObject() && Old->GetObject()->GetStore() != 0 && Old->GetObject()->GetStore() == New->GetObject()->GetStore()) { // Both source and dest are local folders... // This is really an optimization to reduce the overhead of moving objects // between folders, a function which is provided by the storage sub-system // does all the work for us. LArray Mv; Mv.Add(GetObject()); Moved = Old->GetObject()->GetStore()->Move(New->GetFldObj(), Mv); if (Moved == Store3Success) { LAssert(!Old->Items.HasItem(this)); LAssert(GetFolder() == New); LAssert(New->Items.HasItem(this)); } } else { if (IsPlaceHolder()) { } else if (New->GetObject() && GetObject() && New->GetObject()->GetStore()) { // Source OR Dest are remote... LDataI *NewObject = New->GetObject()->GetStore()->Create(Type()); if (NewObject) { LDataI *OldObject = GetObject(); // Copy the current data into the new object NewObject->CopyProps(*GetObject()); SetObject(NewObject, false, _FL); // Try writing it to the store... // bool InOld = Old->Items.HasItem(this); Store3Status WrStatus = New->WriteThing(this); switch (WrStatus) { default: case Store3Error: { // It failed, delete the new object... SetObject(OldObject, false, _FL); DeleteObj(NewObject); break; } case Store3Success: { // Ok, immediate save, set new object // delete old object Moved = OldObject->Delete(false); if (Moved == Store3Error) { SetObject(OldObject, false, _FL); DeleteObj(NewObject); } else if (Moved == Store3Success) { // Remove the Thing from the old folder. Old->Items.Delete(this); LAssert(New->Items.HasItem(this)); if (GetList()) GetList()->Remove(this); } else // Delayed { // Because the list item is the Mail object itself we can't leave a // place holder in the LList until the delayed delete happens. The // mail object is need to appear in the destination folder, as it's // now associated with 'NewObject'. Old->Items.Delete(this); if (GetList()) GetList()->Remove(this); } break; } case Store3Delayed: { // We have to wait for the object to be written. // There will be a ScribeWnd::OnNew(...) call back // when that happens. // // If is succeeds: // - we need to swap the objects over... complete // the updating of the UI. // // If it fails: // - do nothing... // // In the meantime change the object back to the old // one. But leave the UserData pointing to us. This // is so the OnNew handler can finish the move for // us later, and still know whats going on. LAssert(Old->Items.HasItem(this)); // The old folder needs to have // a pointer to us until "OnNew". SetObject(OldObject, false, _FL); // Setup a new Thing for the new Object... Thing *t = App->CreateThingOfType(Type(), NewObject); if (t) { // Add it to the new folder... New->Items.Add(t); // Setup a delete operation to be executed when the object arrives // back at the app with an OnNew events. t->DeleteOnAdd.Path = Old->GetPath(); t->DeleteOnAdd.Obj = this; } Moved = WrStatus; break; } } } } } } else { Moved = New->WriteThing(this); } } return Moved; } void Thing::OnCreate() { } bool Thing::OnDelete() { if (!App) return false; if (IsPlaceHolder()) { DecRef(); return true; } Mail *m = IsMail(); if (m) { List Lst; Lst.Insert(m); App->OnNewMail(&Lst, false); } if (GetObject() && GetObject()->GetStore()) { LArray Del; Del.Add(GetObject()); if (GetObject()->GetStore()->Delete(Del, true)) { return true; } } ScribeFolder *Trash = App->GetFolder(FOLDER_TRASH); if (!Trash) return false; LArray Items; Items.Add(this); - return Trash->MoveTo(Items); + + // FIXME: Impl callback + Trash->MoveTo(Items, false); + return true; } void Thing::OnMove() { /* if (LListItem::Parent) { int MyIndex = LListItem::Parent->IndexOf(this); LListItem::Parent->Remove(this); if (Parent && Parent->Length() < 1) { Window->OnSelect(); } else if (LListItem::Parent && MyIndex >= 0) { LListItem::Parent->Value(MyIndex); } } */ } bool Thing::OnBeginDrag(LMouse &m) { int Ico = -1; switch (Type()) { case MAGIC_MAIL: Ico = ICON_UNREAD_MAIL; break; case MAGIC_CONTACT: Ico = ICON_CONTACT; break; case MAGIC_FILTER: Ico = ICON_FILTER; break; case MAGIC_CALENDAR: Ico = ICON_CALENDAR; default: break; } if (Ico >= 0) { LImageList *s = App->GetIconImgList(); if (s) { LRect r; r.ZOff(s->TileX()-1, s->TileY()-1); r.Offset(s->TileX() * Ico, 0); SetIcon(s, &r); } } Drag(App, m.Event, DROPEFFECT_MOVE | DROPEFFECT_COPY); SetIcon(NULL); return true; } bool Thing::GetFormats(LDragFormats &Formats) { Formats.Supports(ScribeThingList); #if !defined(LGI_COCOA) Formats.Supports(LGI_FileDropFormat); #else Formats.Supports(LGI_StreamDropFormat); #endif return Formats.Length() > 0; } void Thing::ExportAll( LViewI *Parent, const char *ExportMimeType, std::function Callback) { List Sel; if (GetList()) GetList()->GetSelection(Sel); else Sel.Insert(this); - auto Process = [&](LFileSelect *Select) + auto Process = [this, Callback, Parent, Sel, ExportMimeType=LString(ExportMimeType)](LFileSelect *Select) { int Exported = 0; int Errors = 0; - for (auto m: Sel) + for (unsigned idx = 0; idx < Sel.Length(); idx++) { + auto m = Sel[idx]; const char *Out; char Buf[MAX_PATH_LEN]; if (Sel.Length() == 1) { Out = Select->Name(); } else { char *Leaf = LGetLeaf(m->GetDropFileName()); if (!Leaf) { Errors++; continue; } // Make a unique name... for (int Index = 1; Index < 1000; Index++) { LString Nm = Leaf; if (Index > 1) { LString::Array a = Nm.RSplit(".", 1); if (a.Length() == 2) Nm.Printf("%s %i.%s", a[0].Get(), Index, a[1].Get()); else Nm.Printf("%s %i", a[0].Get(), Index); } if (!LMakePath(Buf, sizeof(Buf), Select->Name(), Nm)) { Errors++; break; } if (!LFileExists(Buf)) break; } Out = Buf; } LAutoPtr f(new LFile); if (!f->Open(Out, O_WRITE)) { LgiTrace("%s:%i - Couldn't open '%s' for writing.", _FL, Select->Name()); Errors++; } else { f->SetSize(0); if (m->Export(m->AutoCast(f), ExportMimeType)) Exported++; else Errors++; } } if (Errors > 0) LgiMsg(Parent, "Export failed: %i exported, %i errors.", AppName, MB_OK, Exported, Errors); if (Callback) Callback(Errors == 0); }; auto Select = new LFileSelect(Parent); if (Sel.Length() == 1) Select->Name(LGetLeaf(GetDropFileName())); Select->Type("Email", "*.eml"); if (Sel.Length() > 1) { - Select->OpenFolder([&](auto dlg, auto status) + Select->OpenFolder([this, Process](auto dlg, auto status) { if (status) Process(dlg); delete dlg; }); } else { - Select->Save([&](auto dlg, auto status) + Select->Save([this, Process](auto dlg, auto status) { if (status) Process(dlg); delete dlg; }); } } bool Thing::CallMethod(const char *MethodName, LVariant *ReturnValue, LArray &Args) { ScribeDomType Fld = StrToDom(MethodName); switch (Fld) { case SdImport: // Type: (String FileName, String MimeType) { *ReturnValue = false; if (Args.Length() != 2) LgiTrace("%s:%i - Error: expecting 2 arguments to 'Import'.\n", _FL); else { auto FileName = Args[0]->Str(); LAutoPtr f(new LFile); if (f->Open(FileName, O_READ)) { auto status = Import(AutoCast(f), Args[1]->Str()); *ReturnValue = status.status; } else LgiTrace("%s:%i - Error: Can't open '%s' for reading.\n", _FL, FileName); } break; } case SdExport: // Type: (String FileName, String MimeType) { *ReturnValue = false; if (Args.Length() != 2) LgiTrace("%s:%i - Error: expecting 2 arguments to 'Export'.\n", _FL); else { auto FileName = Args[0]->Str(); LAutoPtr f(new LFile); if (f->Open(FileName, O_WRITE)) { auto status = Export(AutoCast(f), Args[1]->Str()); *ReturnValue = status.status; } else LgiTrace("%s:%i - Error: Can't open '%s' for writing.\n", _FL, FileName); } break; } default: return false; } return true; } bool Thing::GetData(LArray &Data) { bool Status = false; LArray Objs; LList *ParentList = LListItem::Parent; if (ParentList) ParentList->GetSelection(Objs); for (unsigned idx = 0; idx < Data.Length(); idx++) { LDragData &dd = Data[idx]; if (!dd.Format) continue; if (dd.IsFormat(LGI_FileDropFormat)) { LMouse m; App->GetMouse(m, true); LString::Array Files; for (auto t: Objs) Status |= t->GetDropFiles(Files); if (Status && CreateFileDrop(&dd, m, Files)) Status = true; } else if (dd.IsFormat(LGI_StreamDropFormat)) { for (auto t: Objs) { if (t->GetObject()) { LAutoStreamI s = t->GetObject()->GetStream(_FL); if (s) { s->SetPos(0); auto Fn = t->GetDropFileName(); auto MimeType = Store3ItemTypeToMime(t->Type()); dd.AddFileStream(LGetLeaf(Fn), MimeType, s); } } } Status = dd.Data.Length() > 0; } else if (dd.IsFormat(ScribeThingList)) { ScribeClipboardFmt *Fmt = ScribeClipboardFmt::Alloc(Objs); if (Fmt) { Status |= dd.Data[0].SetBinary(Fmt->Sizeof(), Fmt); free(Fmt); } } } return Status; } //////////////////////////////////////// bool Thing::SetField(int Field, int n) { return GetObject() ? GetObject()->SetInt(Field, n) != 0 : false; } bool Thing::SetField(int Field, double n) { LAssert(!"Not implemented"); return false; } bool Thing::SetField(int Field, char *n) { return GetObject() ? GetObject()->SetStr(Field, n) != 0 : false; } bool Thing::SetField(int Field, LDateTime &n) { return GetObject() ? GetObject()->SetDate(Field, &n) != 0 : false; } bool Thing::SetDateField(int Feild, LVariant &v) { auto obj = GetObject(); if (!obj) return false; if (v.Type == GV_DATETIME) { if (!v.Value.Date || !v.Value.Date->IsValid()) return false; return obj->SetDate(FIELD_DATE_MODIFIED, v.Value.Date) >= Store3Delayed; } LDateTime dt(v.Str()); if (!dt.IsValid()) return false; return obj->SetDate(FIELD_DATE_MODIFIED, &dt) >= Store3Delayed; } bool Thing::GetField(int Field, int &n) { n = GetObject() ? (int)GetObject()->GetInt(Field) : 0; return true; } bool Thing::GetField(int Field, double &n) { LAssert(!"Not implemented"); return false; } bool Thing::GetField(int Field, const char *&n) { n = GetObject() ? GetObject()->GetStr(Field) : 0; return n != 0; } bool Thing::GetField(int Field, LDateTime &n) { const LDateTime *t = GetObject() ? GetObject()->GetDate(Field) : 0; if (t) { n = *t; return n.IsValid(); } return false; } bool Thing::GetDateField(int Field, LVariant &v) { auto obj = GetObject(); if (!obj) return false; auto dt = obj->GetDate(Field); if (!dt || !dt->IsValid()) return false; v = dt; return true; } bool Thing::DeleteField(int Field) { LAssert(!"Not implemented"); return false; } ////////////////////////////////////////////////////////////////////////////// LArray ThingUi::All; ThingUi::ThingUi(Thing *item, const char *name) { _Dirty = false; _Running = false; _Name = NewStr(name); _Item = item; App = item ? item->App : NULL; LAssert(App != NULL); SetQuitOnClose(false); SetSnapToEdge(true); Name(_Name); All.Add(this); } ThingUi::~ThingUi() { LAssert(InThread()); LAssert(All.HasItem(this)); All.Delete(this); _Running = false; _Dirty = false; DeleteArray(_Name); } bool ThingUi::OnViewKey(LView *v, LKey &k) { bool IsPopup = false; #ifdef __GTK_H__ OsView Hnd = GtkCast(GetWindow()->WindowHandle(), gtk_widget, GtkWidget); #else OsView Hnd = Handle(); #endif for (LViewI *p = v; p; p = p->GetParent()) { if (dynamic_cast(p)) { IsPopup = true; break; } } bool Status = LWindow::OnViewKey(v, k); // The 'this' pointer may not be valid from here on. if (IsPopup) { return Status; } if (!Status && k.Down() && k.vkey == LK_ESCAPE) { LPostEvent(Hnd, M_CLOSE); return false; } return Status; } bool ThingUi::SetDirty(bool d, bool ui) { bool Status = true; if (d ^ _Dirty) { if (d) { if (_Item && dynamic_cast(_Item->GetObject())) { LAssert(!"Should imap mail become dirty?"); } _Dirty = true; OnDirty(_Dirty); } else { int Result = ui ? LgiMsg(this, LLoadString(IDS_SAVE_ITEM), AppName, MB_YESNOCANCEL) : IDYES; if (Result == IDYES) { OnSave(); _Dirty = false; OnDirty(_Dirty); } else if (Result == IDCANCEL) { Status = false; } else { // Result == IDNO _Dirty = false; OnDirty(_Dirty); } } if (Status) { char s[256]; if (_Dirty) { sprintf_s(s, sizeof(s), "%s (%s)", _Name, LLoadString(IDS_CHANGED)); } else { strcpy_s(s, sizeof(s), _Name); } Name(s); } } return Status; } bool ThingUi::OnRequestClose(bool OsShuttingDown) { static bool Processing = false; bool Status = false; if (!Processing) { Processing = true; bool Cleaned = SetDirty(false); if (Cleaned) { Status = LWindow::OnRequestClose(OsShuttingDown); } Processing = false; } return Status; } diff --git a/Code/ScribeUtils.cpp b/Code/ScribeUtils.cpp --- a/Code/ScribeUtils.cpp +++ b/Code/ScribeUtils.cpp @@ -1,2133 +1,2099 @@ #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; } 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].Get()); } } 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 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(); - } - } -} diff --git a/Code/ScribeUtils.h b/Code/ScribeUtils.h --- a/Code/ScribeUtils.h +++ b/Code/ScribeUtils.h @@ -1,276 +1,274 @@ #ifndef _SCRIBE_UTILS_H_ #define _SCRIBE_UTILS_H_ #include "lgi/common/DateTime.h" #include "lgi/common/Variant.h" #include "lgi/common/DocView.h" #include "lgi/common/ProgressDlg.h" #include "lgi/common/Store3.h" #include "lgi/common/OAuth2.h" #include "ScribeInc.h" ScribeFunc const char *MimeToUti(const char *Mime); ScribeFunc char *ScribeTempPath(); ScribeFunc char *ScribeInsertFields(const char *Template, LDom *Source); ScribeFunc char *MakeFileName(const char *ContentUtf, const char *Ext); ScribeClass LColour SocketMsgTypeToColour(LSocketI::SocketMsgType flags); ScribeFunc class ContactGroup *LookupContactGroup(class ScribeWnd *App, const char *Name); ScribeExtern LString AskOverwriteMsg(const char *FileName); ScribeFunc void PatternBox(LSurface *pDC, const LRect &r); ScribeExtern LOAuth2::Params GetOAuth2Params(const char *Host, Store3ItemTypes Context); ScribeFunc const char *ScribeResourcePath(); ScribeExtern LString::Array ScribeThemePaths(); ScribeExtern LString DetectCharset(LString s); -ScribeExtern void WaitForVariant(LVariant &var); -ScribeExtern void WaitForString(LString &var); extern LAutoString ConvertThreadIndex(char *ThreadIndex, int TruncateChars = 0); /// Parses HTML into a tree and then evaluates 'SearchExp' on each node. /// On the list of matching nodes it evaluates 'ResultExp' using the scripting /// engine. The resulting variables are stored in 'ReturnValue' as a list. extern bool SearchHtml ( /// List of values created from evaluating 'ResultExp' on the matching elements. LVariant *ReturnValue, /// The input HTML to parse. const char *Html, /// A LGI scripting expression. The element being inspected has the following /// variables: /// 'element' - the name of the element. e.g. 'div', 'body' etc. /// 'content' - any textual content after the element in the document. /// 'attr[name]' - the value of an attribute called 'name'. /// If the expression evaluates to non-zero the element is stored in a result /// array. const char *SearchExp, /// The result array is then evaluated into values that can be passed back to /// scripts via this expression. It uses the same available fields as the /// 'SearchExp'. const char *ResultExp ); extern void LHtmlMsg ( /// The callback to receive the status std::function Callback, /// The parent view or NULL if none available LViewI *Parent, /// The message's text. This is a printf format string that you can pass arguments to const char *Html, /// The title of the message box window const char *Title = 0, /// The type of buttons below the message. Can be one of: /// #MB_OK, #MB_OKCANCEL, #MB_YESNO or #MB_YESNOCANCEL. int Type = MB_OK, ... ); extern void ClearTempPath(); extern char *RemoveAmp(const char *s); extern LString AddAmp(const char *menu, int shortcut); class HttpImageThread : public LThreadWorker, public LCancel { class ScribeWnd *App; LString Proxy, Cache; LHashTbl,LString> UriMap; LAutoPtr z; public: HttpImageThread(ScribeWnd *app, const char *proxy, LThreadTarget *First); ~HttpImageThread(); void DoJob(LThreadJob *j); }; class Store3Progress : public LProgressDlg, public LDataPropI { bool Interact; int NewFormat; LString Err, Cache; public: Store3Progress(LView *parent, bool interact); const char *GetStr(int id); Store3Status SetStr(int id, const char *str); int64 GetInt(int id); Store3Status SetInt(int id, int64 i); }; ScribeFunc void TraceTime(char *s); class CountItem { public: int Type; int64 Count; CountItem() { Type = 0; Count = 0; } }; class Counter : public List { CountItem *FindType(int Type); public: Counter() {} ~Counter(); void Inc(int Type); void Dec(int Type); void Add(int Type, int64 n); void Sub(int Type, int64 n); int64 GetTypeCount(int Type); }; class Mailto { public: ScribeWnd *App; List To; char *Subject; char *Body; Mailto(ScribeWnd *app, const char *s); ~Mailto(); void Apply(class Mail *m); }; class LStringStream : public LStringPipe { LStream *s; ssize_t Read(void *Ptr, ssize_t Size, int Flags = 0) { return s->Read(Ptr, Size, Flags); } ssize_t Write(const void *Ptr, ssize_t Size, int Flags = 0) { return s->Write(Ptr, Size, Flags); } public: LStringStream(LStream *str) { s = str; s->SetPos(0); } bool IsOpen() { return s->IsOpen(); } int Close() { return s->Close(); } bool IsEmpty() { return s->GetSize() == 0; } void Empty() { s->SetSize(0); } int64 GetSize() { return s->GetSize(); } void *New(ssize_t AddBytes = 0) { char *Buf = 0; auto Len = s->GetSize(); if (Len > 0) { Buf = new char[Len + AddBytes]; if (Buf) { s->Read(Buf, Len); memset(Buf+Len, 0, AddBytes); } } else { LAssert(0); } return Buf; } int64 Peek(uchar *Ptr, ssize_t Size) { LAssert(0); return 0; } int64 Peek(LStreamI *Ptr, ssize_t Size) { LAssert(0); return 0; } }; class TabDialog : public LDialog { int TabCtrlId; int HelpBtnId; void IdealSize(LButton *b); public: TabDialog(int tabCtrlId, int helpBtnId) { TabCtrlId = tabCtrlId; HelpBtnId = helpBtnId; } void OnCreate(); void OnPosChange(); }; class ProtocolSettingStore : public LDom { LOptionsFile *Opts; LString AccountTag; public: ProtocolSettingStore(LOptionsFile *opts, const char *accountTag) { Opts = opts; AccountTag = accountTag; } LOptionsFile *GetOptions() { return Opts; } bool GetVariant(const char *Name, LVariant &Value, const char *Array = NULL) override { char s[256]; sprintf_s(s, sizeof(s), "%s.%s", AccountTag.Get(), Name); LAssert(Array == NULL); // Shouldn't need this return Opts->GetValue(s, Value); } bool SetVariant(const char *Name, LVariant &Value, const char *Array = NULL) override { char s[256]; sprintf_s(s, sizeof(s), "%s.%s", AccountTag.Get(), Name); LAssert(Array == NULL); // Shouldn't need this return Opts->SetValue(s, Value); } }; class ScriptDownloadContentThread : public LThread, public LCancel { ScribeWnd *App; LString Uri; LString CallbackName; LStringPipe Out; LString Err; LVariant UserData; bool Result = false; public: ScriptDownloadContentThread(ScribeWnd *App, LString Uri, LString CallbackName, LVariant *userData = NULL); int Main(); void OnComplete(); }; #endif diff --git a/Code/Scripting.cpp b/Code/Scripting.cpp --- a/Code/Scripting.cpp +++ b/Code/Scripting.cpp @@ -1,1138 +1,1139 @@ #include "Scribe.h" #include "ScribePrivate.h" #include "lgi/common/Scripting.h" #include "lgi/common/TextView3.h" #include "lgi/common/ThreadEvent.h" #include "lgi/common/LgiRes.h" #include "resdefs.h" #include "../src/common/Coding/ScriptingPriv.h" extern LHostFunc Methods[]; #ifdef _MSC_VER #define __func__ __FUNCTION__ #endif #define ARG_CHECK(op, num) \ if (Args.Length() op num) \ { \ Args.Throw(_FL, "%s: Wrong number of args: %i (expecting %i)\n", __func__, Args.Length(), num); \ *Args.GetReturn() = false; \ return true; \ } LScribeScript *LScribeScript::Inst = 0; LView *CastLView(LVariant *v) { if (v) { if (v->Type == GV_DOM) return dynamic_cast(v->Value.Dom); else if (v->Type == GV_GVIEW) return v->Value.View; } return 0; } ScribeFolder *CastFolder(LVariant *v) { if (v && v->Type == GV_DOM) return dynamic_cast(v->Value.Dom); return NULL; } void LScribeScriptPriv_ConsoleClosingCallback(LScriptConsole *Console, void *UserData); struct LScribeScriptPriv : public LStream, public LThread { ScribeWnd *App; bool Loop; LThreadEvent Event; // Incoming buffer LMutex InputLock; // Covers Used and Buffer ssize_t Used; LArray Buffer; // Outputs LMutex OutputLock; // Covers LogFile, LogMem and Console LAutoString LogFile; LArray LogMem; ssize_t LogMemUsed; LScriptConsole *Console; LScribeScriptPriv(ScribeWnd *app) : LThread("LScribeScriptPriv.Thread"), App(app), InputLock("LScribeScriptPriv.Input"), OutputLock("LScribeScriptPriv.Output") { Loop = true; Console = NULL; LogMem.Length(64 << 10); LogMemUsed = 0; // Get the log path char p[MAX_PATH_LEN]; if (!LgiTraceGetFilePath(p, sizeof(p))) { LAssert(0); } else if (!LogFile.Reset(NewStr(p))) { LAssert(0); } // Setup our buffer first Used = 0; Buffer.Length(64 << 10); // Now register to get trace messages... LgiTraceSetStream(this); // Start thread Run(); } ~LScribeScriptPriv() { Loop = false; LgiTraceSetStream(NULL); DeleteObj(Console); Event.Signal(); while (!IsExited()) LSleep(true); } /// This method can't block at all, and must accept data from any /// thread. ssize_t Write(const void *Data, ssize_t Size, int Flags = 0) { ssize_t Status = 0; LAssert(Size < (16 << 10)); if (Data && InputLock.LockWithTimeout(500, _FL)) { // Put all data into a buffer as quickly as possible ssize_t Remaining = (ssize_t)Buffer.Length() - Used; ssize_t Common = MIN(Remaining, Size); char *Ptr = &Buffer[Used]; if (Ptr) { memcpy(Ptr, Data, Common); Status = Common; Used += Common; } InputLock.Unlock(); if (Status > 0) Event.Signal(); } return Status; } void ShowScriptingWindow(bool Show) { LMutex::Auto Lock(&OutputLock, _FL); if (Show) { if (!Console) { Console = new LScriptConsole(App, LScribeScriptPriv_ConsoleClosingCallback, this); if (Console) Console->Write(&LogMem[0], LogMemUsed); } } else { DeleteObj(Console); } } int Main() { while (Loop) { Event.Wait(); if (!Loop) // Shutting down? break; // Check for input data LArray Input; if (InputLock.LockWithTimeout(500, _FL)) { if (Used > 0) { if (Input.Length(Used)) memcpy(&Input[0], &Buffer[0], Used); Used = 0; } InputLock.Unlock(); } if (Input.Length()) { // We have some input... send to outputs if (OutputLock.LockWithTimeout(500, _FL)) { // Write to our log file... LFile f; if (LogFile && f.Open(LogFile, O_WRITE)) { f.SetPos(f.GetSize()); f.Write(&Input[0], Input.Length()); f.Close(); } if (LogMemUsed + Input.Length() > LogMem.Length()) { // Move data down to make space... by deleting a 1/4 of the data // and shifting down the rest. auto i = LogMemUsed >> 2; // Seek to the end of a line for (; i < LogMemUsed; i++) { if (LogMem[i] == '\n') { i++; break; } } // Shift the memory down, and update the used variable memmove(&LogMem[0], &LogMem[i], LogMemUsed-i); LogMemUsed -= i; // Check there is enough space now...? if (LogMemUsed + Input.Length() > LogMem.Length()) // Nope... make it bigger then LogMem.Length(LogMemUsed + Input.Length()); } if (LogMemUsed + Input.Length() < LogMem.Length()) { memcpy(&LogMem[LogMemUsed], &Input[0], Input.Length()); LogMemUsed += Input.Length(); } if (Console) { Console->Write((const char*)&Input[0], Input.Length()); } OutputLock.Unlock(); // Tell the UI about the new messages... #if LGI_VIEW_HANDLE if (App->Handle()) #endif App->PostEvent(M_NEW_CONSOLE_MSG); } } } return 0; } }; void LScribeScriptPriv_ConsoleClosingCallback(LScriptConsole *Console, void *UserData) { ((LScribeScriptPriv*)UserData)->Console = NULL; } LScribeScript::LScribeScript(ScribeWnd *app) { d = new LScribeScriptPriv(app); App = app; Eng = 0; } LScribeScript::~LScribeScript() { DeleteObj(d); } void LScribeScript::ShowScriptingWindow(bool show) { d->ShowScriptingWindow(show); } LStream *LScribeScript::GetLog() { return d; } LAutoString LScribeScript::GetDataFolder() { return App->GetDataFolder(); } LString LScribeScript::GetIncludeFile(const char *FileName) { LString Path; const char *Search[] = { "./Scripts", "../Scripts", "../../Scripts", NULL }; LFile::Path p(ScribeResourcePath()); p += FileName; for (int i=0; Search[i] && !p.Exists(); i++) { p = ScribeResourcePath(); p += Search[i]; p += FileName; } if (p.Exists()) Path = p.GetFull(); else Path = LFindFile(FileName); if (!Path) { LgiTrace("%s:%i - GetIncludeFile(%s) failed.\n", _FL, FileName); return NULL; } LFile f(Path); if (!f) { LgiTrace("%s:%i - GetIncludeFile(%s) couldn't open '%s' for reading.\n", _FL, FileName, Path.Get()); return NULL; } return f.Read(); } LHostFunc *LScribeScript::GetCommands() { return Methods; } void LScribeScript::SetEngine(LScriptEngine *eng) { } bool LScribeScript::GetFolder(LScriptArguments &Args) { ARG_CHECK(!=, 1); Args.GetReturn()->Empty(); if (Args[0]->Type == GV_INT32) { ScribeFolder *f = App->GetFolder(Args[0]->Value.Int); if (f) { *Args.GetReturn() = (LDom*)f; return true; } } char *f = Args[0]->Str(); if (f) { *Args.GetReturn() = (LDom*)App->GetFolder(f); } return true; } bool LScribeScript::GetSourceFolders(LScriptArguments &Args) { ARG_CHECK(!=, 1); LArray Folders; auto FolderType = Args[0]->CastInt32(); Store3ItemTypes ItemType = MAGIC_NONE; switch (FolderType) { case FOLDER_INBOX: case FOLDER_OUTBOX: case FOLDER_SENT: case FOLDER_TRASH: case FOLDER_TEMPLATES: case FOLDER_SPAM: Folders.Add(App->GetFolder(FolderType)); break; case FOLDER_CONTACTS: ItemType = MAGIC_CONTACT; break; case FOLDER_FILTERS: ItemType = MAGIC_FILTER; break; case FOLDER_CALENDAR: ItemType = MAGIC_CALENDAR; break; case FOLDER_GROUPS: ItemType = MAGIC_GROUP; break; } if (ItemType != MAGIC_NONE) Folders = App->GetThingSources(ItemType); auto r = Args.GetReturn(); if (!r->SetList()) return false; auto Lst = r->Value.Lst; for (auto f: Folders) Lst->Add(new LVariant((LDom*)f)); return true; } bool LScribeScript::BrowseFolder(LScriptArguments &Args) { ARG_CHECK(<, 4); LView *Parent = CastLView(Args[0]); LString MessageTxt = Args[1]->Str(); LString DefaultFolderName = Args[2]->Str(); LString CallbackName = Args[3]->Str(); if (!CallbackName) { *Args.GetReturn() = false; return true; } *Args.GetReturn() = true; auto d = new FolderDlg( Parent, App, MAGIC_ANY, // limit to 0, // root 0, // init sel true, DefaultFolderName, MessageTxt); d->DoModal([this, d, CallbackName](auto dlg, auto id) { if (id) { auto FolderPath = d->Get(); auto cb = App->GetCallback(CallbackName); if (cb.Func) { LVirtualMachine Vm; LScriptArguments Args(&Vm); LVariant vFolderPath = FolderPath; Args.Add(&vFolderPath); App->ExecuteScriptCallback(cb, Args); } else LgiTrace("%s:%i - No callback called '%s'\n", _FL, CallbackName.Get()); } delete dlg; }); return true; } bool LScribeScript::CreateSubFolder(LScriptArguments &Args) { ARG_CHECK(!=, 3); *Args.GetReturn() = false; ScribeFolder *Parent = CastFolder(Args[0]); if (!Parent) return true; char *ChildName = Args[1]->Str(); if (!ChildName) return true; for (ScribeFolder *c = Parent->GetChildFolder(); c; c = c->GetNextFolder()) { auto n = c->GetName(true); if (n.Equals(ChildName)) { *Args.GetReturn() = c; return true; } } char *TypeStr = Args[2]->Str(); if (!TypeStr) return true; int Type = MAGIC_NONE; if (_stricmp(TypeStr, "Mail") == 0) Type = MAGIC_MAIL; else if (_stricmp(TypeStr, "Contact") == 0) Type = MAGIC_CONTACT; else if (_stricmp(TypeStr, "Filter") == 0) Type = MAGIC_FILTER; else if (_stricmp(TypeStr, "Calendar") == 0) Type = MAGIC_CALENDAR; else if (_stricmp(TypeStr, "Group") == 0) Type = MAGIC_GROUP; if (Type >= MAGIC_BASE) { ScribeFolder *Child = Parent->CreateSubDirectory(ChildName, Type); if (Child) { *Args.GetReturn() = Child; return true; } } return true; } bool LScribeScript::SaveThing(LScriptArguments &Args) { ARG_CHECK(!=, 2); ScribeFolder *Folder = dynamic_cast(Args[0]->CastDom()); Thing *T = dynamic_cast(Args[1]->CastDom()); *Args.GetReturn() = Folder && T ? T->Save(Folder) : false; return true; } bool LScribeScript::CreateThing(LScriptArguments &Args) { ARG_CHECK(<, 1); Thing *t; uint32_t Type = Args[0]->CastInt32(); ScribeFolder *Folder = Args.Length() > 1 ? dynamic_cast(Args[1]->CastDom()) : NULL; t = App->CreateItem(Type, Folder, false); *Args.GetReturn() = dynamic_cast(t); return true; } bool LScribeScript::MoveThing(LScriptArguments &Args) { ScribeFolder *To = NULL; Thing *t = NULL; LArray Items; ARG_CHECK(!=, 2); // Resolve the thing... t = dynamic_cast(Args[1]->CastDom()); if (!t) { LgiTrace("%s:%i - MoveThing error: Arg 1 not a DOM object.\n", _FL); goto MoveThingErr; } // Resolve the folder... if (Args[0]->Type == GV_DOM) { To = dynamic_cast(Args[0]->CastDom()); if (!To) LgiTrace("%s:%i - MoveThing error: Arg 0 (object ptr) not a Folder.\n", _FL); } else if (Args[0]->Type == GV_STRING) { char *Path = Args[0]->CastString(); To = t->App->GetFolder(Path); if (!To) LgiTrace("%s:%i - MoveThing error: Arg 0 ('%s') not a valid path to a folder.\n", _FL, Path); } if (!To) goto MoveThingErr; // Move the thing to the folder... Items.Add(t); - *Args.GetReturn() = To->MoveTo(Items); + To->MoveTo(Items, false); + *Args.GetReturn() = true; return true; MoveThingErr: *Args.GetReturn() = false; return true; } bool LScribeScript::LoadFolder(LScriptArguments &Args) { ARG_CHECK(!=, 1); *Args.GetReturn() = false; if (Args[0]->Type == GV_DOM) { ScribeFolder *Folder = dynamic_cast(Args[0]->Value.Dom); if (Folder) *Args.GetReturn() = Folder->LoadThings(); } return true; } bool LScribeScript::LookupContact(LScriptArguments &Args) { ARG_CHECK(!=, 1); auto Email = Args[0]->CastString(); *Args.GetReturn() = (LDom*) Contact::LookupEmail(Email); return true; } bool LScribeScript::MsgBox(LScriptArguments &Args) { LViewI *Parent = CastLView(Args[0]); LgiMsg(Parent, "The 'MsgBox' function is deprecated. Please use 'MessageDlg' (it has the same arguments).", AppName); *Args.GetReturn() = 0; return true; } bool LScribeScript::GetSystemPath(LScriptArguments &Args) { ARG_CHECK(!=, 1); Args.GetReturn()->Empty(); LSystemPath sp = LSP_TEMP; const char *Name; if (Args[0]->IsInt()) sp = (LSystemPath) Args[0]->CastInt32(); else if ((Name = Args[0]->CastString())) { #undef _ #define _(path) if (!_stricmp(Name, #path)) sp = path; _(LSP_ROOT) _(LSP_OS) _(LSP_OS_LIB) _(LSP_TEMP) _(LSP_COMMON_APP_DATA) _(LSP_USER_APP_DATA) _(LSP_LOCAL_APP_DATA) _(LSP_DESKTOP) _(LSP_HOME) _(LSP_USER_APPS) _(LSP_EXE) _(LSP_TRASH) _(LSP_APP_INSTALL) _(LSP_APP_ROOT) _(LSP_USER_DOCUMENTS) _(LSP_USER_MUSIC) _(LSP_USER_VIDEO) _(LSP_USER_DOWNLOADS) _(LSP_USER_LINKS) } else return true; LFile::Path p(sp); *Args.GetReturn() = p.GetFull(); return true; } bool LScribeScript::GetScribeTempPath(LScriptArguments &Args) { *Args.GetReturn() = ScribeTempPath(); return true; } bool LScribeScript::JoinPath(LScriptArguments &Args) { char p[MAX_PATH_LEN] = ""; if (Args.Length() > 0) { bool First = true; for (unsigned i=0; iCastString(); if (s) { if (First) strcpy_s(p, sizeof(p), s); else LMakePath(p, sizeof(p), p, s); First = false; } } } *Args.GetReturn() = p; return true; } bool LScribeScript::DeleteThing(LScriptArguments &Args) { *Args.GetReturn() = false; LArray a; int Ok = 0, Error = 0; LDom *dom; bool ToTrash = false; for (auto Arg: Args) { if (Arg->Type == GV_DOM) { if (!(dom = Arg->CastDom())) continue; Attachment *attachment = dynamic_cast(dom); if (attachment) { Mail *m = attachment->GetOwner(); if (m) { if (m->DeleteAttachment(attachment)) Ok++; else Error++; } else LAssert(!"No owner?"); } else { auto t = dynamic_cast(dom); if (t) a.Add(t); } } else if (Arg->Type == GV_LIST) { auto l = Arg->Value.Lst; if (!l) continue; for (auto i: *l) { if (!(dom = i->CastDom())) continue; auto t = dynamic_cast(dom); if (t) a.Add(t); } } else if (Arg->Type == GV_STRING) { if (Stristr(Arg->Str(), "trash")) ToTrash = true; } } while (a.Length() > 0) { auto t = a[0]; auto Store = t->GetObject()->GetStore(); LArray Del; for (auto it = a.begin(); it != a.end(); ) { t = *it; if (t->GetObject()->GetStore() == Store) { Del.Add(t->GetObject()); a.Delete(it); } else it++; } if (Store->Delete(Del, ToTrash) > Store3Error) Ok++; else Error++; } *Args.GetReturn() = Error == 0; return true; } bool LScribeScript::ShowThingWindow(LScriptArguments &Args) { ARG_CHECK(!=, 1); *Args.GetReturn() = false; if (Args[0]->Type == GV_DOM) { Thing *t = dynamic_cast(Args[0]->CastDom()); if (t) *Args.GetReturn() = t->DoUI(); } return true; } bool LScribeScript::AddToolsMenuItem(LScriptArguments &Args) { ARG_CHECK(!=, 2); *Args.GetReturn() = App->RegisterCallback(LToolsMenu, Args); return true; } bool LScribeScript::AddCallback(LScriptArguments &Args) { ARG_CHECK(<, 2); auto Str = Args[0]->CastString(); ScribeDomType Type = StrToDom(Str); switch (Type) { #define HandleCallbackType(DomField, CbType) \ case DomField: \ *Args.GetReturn() = App->RegisterCallback(CbType, Args); \ break HandleCallbackType(SdOnBeforeMailSend, LMailOnBeforeSend); HandleCallbackType(SdOnAfterMailReceive, LMailOnAfterReceive); HandleCallbackType(SdOnThingContextMenu, LThingContextMenu); HandleCallbackType(SdOnFolderContextMenu, LFolderContextMenu); HandleCallbackType(SdOnThingToolbar, LThingUiToolbar); HandleCallbackType(SdOnApplicationToolbar, LApplicationToolbar); HandleCallbackType(SdOnBeforeInstallBar,LBeforeInstallBar); HandleCallbackType(SdOnInstallComponent, LInstallComponent); HandleCallbackType(SdOnTimer, LOnTimer); HandleCallbackType(SdOnRenderMail, LRenderMail); HandleCallbackType(SdOnLoad, LOnLoad); default: Args.Throw(_FL, "No callback named '%s'", Str); *Args.GetReturn() = false; break; } return true; } // MenuAddItem(SubMenu, IconIndex/File, LabelText, Position, CallbackMethod, CallbackId) bool LScribeScript::MenuAddItem(LScriptArguments &Args) { ARG_CHECK(!=, 6); *Args.GetReturn() = false; LDom *Dom = Args[0]->CastDom(); LScriptUi *Menu = dynamic_cast(Dom); char *IconFile = 0; int IconIdx = -1; if (Args[1]->Type == GV_INT32) IconIdx = Args[1]->CastInt32(); else if (Args[1]->Type == GV_STRING) IconFile = Args[1]->Str(); char *LabelTxt = Args[2]->Str(); int Position = Args[3]->CastInt32(); char *CallbackMethodName = Args[4]->Str(); int CallbackId = Args[5]->CastInt32(); if (!Menu || !Menu->Sub) return Args.Throw(NULL, -1, "No menu to append to."); if (CallbackId < 0) { Menu->Sub->AppendSeparator(Position); *Args.GetReturn() = true; return true; } if (!LabelTxt) return Args.Throw(NULL, -1, "No label text."); if (!CallbackMethodName) return Args.Throw(NULL, -1, "No label callback method name."); LScriptCallback Cb = App->GetCallback(CallbackMethodName); if (!Cb.Func) { Args.Throw(NULL, -1, "Callback not defined."); return true; } auto Top = Menu->Sub; while (Top->GetParent() && Top->GetParent()->GetParent()) Top = Top->GetParent()->GetParent(); auto Existing = Top->FindItem(CallbackId); if (Existing) return Args.Throw(NULL, -1, "Item ID '%i' already exists.", CallbackId); Cb.Param = CallbackId; Menu->Sub->AppendItem(LabelTxt, CallbackId, true, Position); // The callbacks are always in a flat list in the top most LScriptCallback, // where the caller can easily see them. LScriptUi *Root = Menu; while (Root->Parent) Root = Root->Parent; Root->Callbacks.New() = Cb; *Args.GetReturn() = true; return true; } bool LScribeScript::MenuAddSubmenu(LScriptArguments &Args) { ARG_CHECK(!=, 3); Args.GetReturn()->Empty(); LScriptUi *Menu = dynamic_cast(Args[0]->CastDom()); char *SubmenuTxt = Args[1]->Str(); int Position = Args[2]->CastInt32(); if (Menu && Menu->Sub && SubmenuTxt) { LScriptUi *s = new LScriptUi(Menu->Sub->AppendSub(SubmenuTxt, Position)); if (s) { s->Parent = Menu; Menu->Subs.Add(s); *Args.GetReturn() = s; } } return true; } bool LScribeScript::ToolbarAddItem(LScriptArguments &Args) { ARG_CHECK(<, 6); LScriptUi *Tb = dynamic_cast(Args[0]->CastDom()); int Icon = Args[1]->CastInt32(); char *Label = Args[2]->Str(); // int Pos = Args[3]->CastInt32(); char *CallbackMethodName = Args[4]->Str(); int CallbackId = Args[5]->CastInt32(); if (Tb && Tb->Toolbar && CallbackMethodName) { if (CallbackId < 0) { Tb->Toolbar->AppendSeparator(); *Args.GetReturn() = true; } else { LScriptCallback Cb = App->GetCallback(CallbackMethodName); if (Cb.Func) { Cb.Param = CallbackId; Tb->Toolbar->AppendButton(Label, CallbackId, TBT_PUSH, true, Icon); LScriptUi *Root = Tb; while (Root->Parent) Root = Root->Parent; Root->Callbacks.New() = Cb; *Args.GetReturn() = true; } else *Args.GetReturn() = false; } } else *Args.GetReturn() = false; return true; } bool LScribeScript::FilterDoActions(LScriptArguments &Args) { ARG_CHECK(!=, 2); Filter *f = dynamic_cast(Args[0]->CastDom()); Mail *m = dynamic_cast(Args[1]->CastDom()); if (f && m) { bool Stop = false; f->DoActions(m, Stop); *Args.GetReturn() = true; } else *Args.GetReturn() = false; return true; } #define DefFn(Name) \ LHostFunc(#Name, 0, (ScriptCmd)&LScribeScript::Name) LHostFunc Methods[] = { DefFn(MsgBox), DefFn(GetSystemPath), DefFn(GetScribeTempPath), DefFn(JoinPath), DefFn(GetFolder), DefFn(GetSourceFolders), DefFn(LoadFolder), DefFn(CreateSubFolder), DefFn(BrowseFolder), DefFn(CreateThing), DefFn(MoveThing), DefFn(SaveThing), DefFn(DeleteThing), DefFn(ShowThingWindow), DefFn(FilterDoActions), DefFn(LookupContact), DefFn(AddToolsMenuItem), DefFn(AddCallback), DefFn(MenuAddItem), DefFn(MenuAddSubmenu), DefFn(ToolbarAddItem), LHostFunc(0, 0, 0), }; LScriptConsole::LScriptConsole(ScribeWnd *app, ConsoleClosingCallback callback, void *callback_data) { Txt = NULL; Callback = callback; CallbackData = callback_data; App = app; LRect r(0, 0, 600, 500); SetPos(r); if (App) MoveSameScreen(App); else MoveToCenter(); Name(LLoadString(IDS_CONSOLE, "Console")); if (Attach(0)) { Children.Insert(Txt = new LTextLog(100)); if (Txt) { Txt->SetPourLargest(true); } AttachChildren(); Visible(true); RegisterHook(this, LKeyEvents); } } LScriptConsole::~LScriptConsole() { printf("%p::~LScriptConsole() %p\n", this, Callback); if (Callback) Callback(this, CallbackData); } bool LScriptConsole::OnViewKey(LView *v, LKey &k) { if (k.CtrlCmd() && ToLower(k.c16) == 'w') { if (k.Down()) Quit(); return true; } return false; } bool LScriptConsole::OnRequestClose(bool OsShuttingDown) { LVariant v; App->GetOptions()->SetValue(OPT_ShowScriptConsole, v = 0); auto Item = App->GetMenu()->FindItem(IDM_SCRIPTING_CONSOLE); if (Item) Item->Checked(false); if (Callback) { Callback(this, CallbackData); Callback = NULL; } return true; } void LScriptConsole::Write(const char *s, int64 Len) { if (Txt) Txt->Write(s, (int)Len); } bool OnFilterScript(Filter *f, Mail *m, const char *Script) { if (!Script || !f || !m || !LScribeScript::Inst) return false; LScriptEngine *e = m->App->GetScriptEngine(); LAutoPtr obj(new LCompiledCode); if (!obj) { return false; } // Setup the various pre-defined variables LVariant v; obj->Set("App", v = dynamic_cast(f->App)); obj->Set("Mail", v = dynamic_cast(m)); obj->Set("Filter", v = dynamic_cast(f)); // Compile... LString ScriptName; ScriptName.Printf("%s.filter", f->GetName()); if (!e->Compile(obj, NULL, Script, ScriptName)) { f->App->OnScriptCompileError(NULL, f); return false; } // And run... LExecutionStatus Result = e->Run(obj, NULL, ScribeTempPath()); return Result != ScriptError; } bool OnToolScript(ScribeWnd *App, const char *File) { bool Status = false; if (App && File && LScribeScript::Inst) { LAutoString Script(LReadTextFile(File)); if (Script) { LScriptEngine *e = App->GetScriptEngine(); if (e) { LAutoPtr Obj(new LCompiledCode); if (e->Compile(Obj, NULL, Script, File)) { LScriptArguments Args(NULL); Args.Add(new LVariant((LDom*)App)); Status = e->CallMethod(Obj, "Main", Args); Args.DeleteObjects(); } else { App->OnScriptCompileError(File, NULL); } } } } return Status; } /////////////////////////////////////////////////////////////////////////// bool LScriptUi::SetupCallbacks(ScribeWnd *App, ThingUi *Parent, Thing *t, LScriptCallbackType Type) { if (!App) return false; LArray Callbacks; if (!App->GetScriptCallbacks(Type, Callbacks)) return false; LScriptArguments Args(NULL); int a = 0; Args[a++] = new LVariant(App); Args[a++] = new LVariant(this); Args[a++] = new LVariant(t); for (unsigned i=0; iExecuteScriptCallback(*Callbacks[i], Args); return true; } bool LScriptUi::ExecuteCallbacks(ScribeWnd *App, ThingUi *Parent, Thing *t, int Cmd) { if (!App) return false; LScriptArguments Args(NULL); Args[0] = new LVariant(App); // App Args[1] = new LVariant(Parent); // Window Args[2] = new LVariant(t); // Thing Args[3] = new LVariant(Cmd); // CallbackId for (auto &c: Callbacks) { if (c.Param == Cmd) App->ExecuteScriptCallback(c, Args); } return false; } diff --git a/Code/Store3Common.cpp b/Code/Store3Common.cpp --- a/Code/Store3Common.cpp +++ b/Code/Store3Common.cpp @@ -1,532 +1,532 @@ #include #include "lgi/common/Lgi.h" #include "lgi/common/Mail.h" #include "Store3Common.h" #include "ScribeInc.h" #include "DomType.h" LHashTbl,LDataStoreI*> LDataStoreI::Map; ////////////////////////////////////////////////////////////////////////////////////// const char *Store3ItemTypeToMime(Store3ItemTypes type) { switch (type) { case MAGIC_MAIL: return "message/rfc822"; case MAGIC_CONTACT: return "text/vcard"; case MAGIC_ATTACHMENT: return "application/octet-stream"; case MAGIC_CALENDAR: return "text/vcalendar"; case MAGIC_FILTER: return "text/x-email-filter"; default: LAssert(!"Unknown type"); break; } return NULL; } ////////////////////////////////////////////////////////////////////////////////////// LDataUserI::LDataUserI() { Object = NULL; } LDataUserI::~LDataUserI() { if (Object) Object->UserData = NULL; } LDataI *LDataUserI::GetObject() { return Object; } bool LDataUserI::SetObject(LDataI *o, bool InDestuctor, const char *File, int Line) { if (o == Object) return true; if (Object) { Object->UserData = NULL; if (!InDestuctor && Object->IsOrphan()) delete Object; Object = NULL; } Object = o; if (File) SetterRef.Printf("%s:%i", File, Line); else SetterRef.Empty(); if (Object) Object->UserData = this; return true; } ////////////////////////////////////////////////////////////////////////////// Store3Addr::Store3Addr(LDataStoreI *store, LDataPropI *i) { LAssert(store != NULL); Store = store; CC = 0; if (i) CopyProps(*i); } Store3Addr::~Store3Addr() { } size_t Store3Addr::Sizeof() { size_t s = sizeof(*this); if (Addr) s += strlen(Addr); if (Name) s += strlen(Name); return s; } void Store3Addr::SetStore(LDataStoreI *s) { Store = s; LAssert(Store != NULL); } void Store3Addr::Empty() { Name.Empty(); Addr.Empty(); CC = 0; } Store3CopyImpl(Store3Addr) { Empty(); Name = p.GetStr(FIELD_NAME); Addr = p.GetStr(FIELD_EMAIL); CC = (int)p.GetInt(FIELD_CC); return true; } const char *Store3Addr::GetStr(int id) { switch (id) { case FIELD_NAME: return Name; case FIELD_EMAIL: return Addr; } return 0; } Store3Status Store3Addr::SetStr(int id, const char *str) { switch (id) { case FIELD_NAME: { Name = str; break; } case FIELD_EMAIL: { Addr = str; break; } default: return Store3Error; } return Store3Success; } bool Store3Addr::GetVariant(const char *n, LVariant &Value, const char *Array) { ScribeDomType Fld = StrToDom(n); switch (Fld) { case SdName: // Type: String { Value = Name; break; } case SdEmail: // Type: String { Value = Addr; break; } case SdDomain: // Type: String { char *At = Addr ? strchr(Addr, '@') : NULL; if (At) Value = At + 1; else Value.Empty(); break; } case SdText: // Type: String { char s[512]; LString EscName; if (Name) EscName = LString::Escape(Name, -1, "\'"); if (Name && Addr) sprintf_s(s, sizeof(s), "\"%s\" <%s>", EscName.Get(), (char*)Addr); else if (Name) sprintf_s(s, sizeof(s), "\"%s\"", EscName.Get()); else if (Addr) sprintf_s(s, sizeof(s), "<%s>", (char*)Addr); else return false; s[sizeof(s)-1] = 0; Value = s; break; } case SdContact: // Type: Contact { LDataEventsI *e = Store->GetEvents(); if (!e) return false; LArray Matches; if (!e->Match(Store, this, MAGIC_CONTACT, Matches)) return false; Value = Matches[0]; break; } case SdGroups: // Type: String[] { LDataEventsI *e = Store->GetEvents(); if (!e) return false; LArray Matches; if (e->Match(Store, this, MAGIC_GROUP, Matches)) { Value.SetList(); for (unsigned i=0; i v(new LVariant); if (Matches[i]->GetValue("Name", *v)) Value.Value.Lst->Insert(v.Release()); } } else Value.Empty(); break; } default: { LAssert(!"Not a supported field."); return false; } } return true; } bool Store3Addr::SetVariant(const char *n, LVariant &Value, const char *Array) { ScribeDomType Fld = StrToDom(n); switch (Fld) { case SdName: // Type: String { Name = Value.Str(); break; } case SdEmail: // Type: String { Addr = Value.Str(); break; } case SdText: // Type: String { - DecodeAddrName(Value.Str(), [&](LString name, LString addr){ + DecodeAddrName(Value.Str(), [this](LString name, LString addr){ Name = LString::UnEscape(name); Addr = addr; }, NULL); break; } default: { LAssert(!"Not a valid field"); return false; } } return true; } int64 Store3Addr::GetInt(int id) { switch (id) { case FIELD_CC: return CC; } return -1; } Store3Status Store3Addr::SetInt(int id, int64 i) { switch (id) { case FIELD_CC: CC = (int)i; break; default: return Store3Error; } return Store3Success; } /////////////////////////////////////////////////////////////////////////////////// Store3Field::Store3Field(LDataStoreI *Store, int id, int width) { Id = id; Width = width; } const char *Store3Field::GetStr(int id) { return 0; } int64 Store3Field::GetInt(int id) { switch (id) { case FIELD_ID: return Id; case FIELD_WIDTH: return Width; } return -1; } Store3Status Store3Field::SetInt(int id, int64 i) { switch (id) { case FIELD_ID: Id = (int)i; break; case FIELD_WIDTH: Width = (int)i; break; default: return Store3Error; } return Store3Success; } //////////////////////////////////////////////////////////////////////////////// // Mime conversion bool Store3ToGMime(LMime *Out, LDataPropI *InInterface) { LDataI *In = dynamic_cast(InInterface); if (!Out || !In) { LAssert(0); return false; } int Type = In->Type(); if (Type == MAGIC_MAIL) { LDataIt Sub = In->GetList(FIELD_MIME_SEG); LDataPropI *Child = Sub->First(); if (Child) { if (!Store3ToGMime(Out, Child)) return false; } else LAssert(0); } else if (Type == MAGIC_ATTACHMENT) { auto Hdrs = In->GetStr(FIELD_INTERNET_HEADER); if (Hdrs) { if (!Out->SetHeaders(Hdrs)) { LAssert(0); return false; } } else { // No headers??? } LAutoStreamI Data = In->GetStream(_FL); if (Data) { if (!Out->SetData(true, Data.Release())) { LAssert(0); return false; } } LDataIt Sub = In->GetList(FIELD_MIME_SEG); for (LDataPropI *Child = Sub->First(); Child; Child = Sub->Next()) { LMime *NewSeg = Out->NewChild(); if (NewSeg) { if (Store3ToGMime(NewSeg, Child)) Out->Insert(NewSeg); else return false; } else { LAssert(0); return false; } } } else { LAssert(!"Incorrect object type."); return false; } return true; } bool GMimeToStore3(LDataPropI *Out, LMime *In, bool InMemOnly) { LDataI *DataOut = dynamic_cast(Out); if (!DataOut || !In) { LAssert(0); return false; } if (!Out->SetStr(FIELD_INTERNET_HEADER, In->GetHeaders())) { LAssert(0); return false; } if (In->GetLength() > 0) { LAutoStreamI Data(new LMemStream(In->GetData(), -1, -1)); if (!Data) { LAssert(0); return false; } if (!DataOut) { LAssert(0); return false; } DataOut->SetStream(Data); } for (int i=0; iLength(); i++) { LDataI *cOut = DataOut->GetStore()->Create(MAGIC_ATTACHMENT); if (!cOut) return false; LMime *cIn = (*In)[i]; if (!GMimeToStore3(cOut, cIn)) return false; Store3Status s = cOut->Save(DataOut); if (s == Store3Error) { LAssert(0); return false; } } return true; } //////////////////////////////////////////////////////////////////////////////////////// LString HeadersFromStream(LStreamI *Msg) { LString s; for (int Sz = 1024; Msg && Sz < (64 << 10); Sz += 1024) { Msg->SetPos(0); if (!s.Length(Sz)) break; ssize_t Rd = Msg->Read(s.Get(), s.Length()); if (Rd <= 0) { s.Empty(); break; } s.Length(Rd); ptrdiff_t EndOfHeader = s.Find("\r\n\r\n"); if (EndOfHeader > 0) { s.Length(EndOfHeader); break; } } return s; } //////////////////////////////////////////////////////////////////////////////////////// LString CreateMboxHeader(LDataI *Object) { LString s; LDataPropI *From; if (!Object || !(From = Object->GetObj(FIELD_FROM))) { LAssert(0); return s; } // generate from header s.Printf("From %s ", From->GetStr(FIELD_EMAIL)); struct tm Ft; ZeroObj(Ft); LDateTime Rec = *Object->GetDate(FIELD_DATE_RECEIVED); if (!Rec.Year()) Rec.SetNow(); Ft.tm_sec = Rec.Seconds(); /* seconds after the minute - [0,59] */ Ft.tm_min = Rec.Minutes(); /* minutes after the hour - [0,59] */ Ft.tm_hour = Rec.Hours(); /* hours since midnight - [0,23] */ Ft.tm_mday = Rec.Day(); /* day of the month - [1,31] */ Ft.tm_mon = Rec.Month() - 1; /* months since January - [0,11] */ Ft.tm_year = Rec.Year() - 1900; /* years since 1900 */ Ft.tm_wday = Rec.DayOfWeek(); char Temp[64]; strftime(Temp, sizeof(Temp), "%a %b %d %H:%M:%S %Y", &Ft); s += Temp; s += "\r\n"; return s; } diff --git a/Code/Store3Mail2/Store3Mail2.cpp b/Code/Store3Mail2/Store3Mail2.cpp --- a/Code/Store3Mail2/Store3Mail2.cpp +++ b/Code/Store3Mail2/Store3Mail2.cpp @@ -1,475 +1,475 @@ #include "Store3Mail2.h" #include "resdefs.h" ///////////////////////////////////////////////////////////////////////////////////// Store3Status ThingData::Delete() { LArray Lst; Lst.Add(this); Kit->Callback->OnDelete(GetParent(), Lst); if (Store && Store->GetTree()->SeparateItem(Store)) { FolderData *Parent = GetParent(); if (Parent) { Parent->Things.Delete(this); delete this; } return Store3Success; } return Store3Error; } Store3Status ThingData::Save(LDataI *Folder) { if (!Store) { FolderData *f = dynamic_cast(Folder); if (f && f->Store) { StorageItem *n = f->Store->CreateSub(this); if (n) { Store = n; LAssert(f->Things.IndexOf(this) < 0); f->Things.Insert(this); LArray lst; lst.Add(this); Kit->Callback->OnNew(f, lst, -1, false); return Store3Success; } // else we have been deleted. } else LAssert(!"Not a valid mail2 folder."); } else { return Store->Save() ? Store3Success : Store3Error; } LAssert(0); return Store3Error; } GAutoStreamI ThingData::GetStream(const char *file, int line) { GAutoStreamI Ret; if (Store) { Ret.Reset(Store->GotoObject(file, line)); } else LAssert(0); return Ret; } ///////////////////////////////////////////////////////////////////////////////////// LMail2Store::LMail2Store(char *file, LDataEventsI *callback) : Storage2::StorageKitImpl(file) { Callback = callback; Mailbox = 0; if (!FileExists(file)) { char Msg[MAX_PATH_LEN]; sprintf_s(Msg, sizeof(Msg), LLoadString(IDS_ERROR_FILE_DOESNT_EXIST), file); ErrorMsg.Reset(NewStr(Msg)); } } LMail2Store::~LMail2Store() { } uint64 LMail2Store::Size() { return GetFileSize(); } char *LMail2Store::GetStr(int id) { switch (id) { case FIELD_ERROR: return ErrorMsg; case FIELD_STORE_PASSWORD: { - GPassword p; + LPassword p; if (GetPassword(&p)) { static char s[128]; p.Get(s); return s; } break; } } return 0; } bool LMail2Store::SetStr(int id, const char *str) { return 0; } int64 LMail2Store::GetInt(int id) { switch (id) { case FIELD_READONLY: return StorageKitImpl::GetReadOnly(); case FIELD_STATUS: return StorageKitImpl::GetStatus() ? DsOk : DsError; case FIELD_VERSION: return 2; } return -1; } bool LMail2Store::SetInt(int id, int64 i) { return 0; } LDataI *LMail2Store::Create(int Type) { switch (Type) { case MAGIC_FOLDER: return new FolderData(this); case MAGIC_MAIL: return new MailData(this); case MAGIC_CONTACT: return new ContactData(this); case MAGIC_CALENDAR: return new CalendarData(this); case MAGIC_FILTER: return new FilterData(this); case MAGIC_ATTACHMENT: return new AttachmentData(this); case MAGIC_GROUP: return new GroupData(this); } return 0; } LDataFolderI *LMail2Store::GetRoot(bool Create) { if (!Mailbox && GetStatus()) { Mailbox = new FolderData(this); Mailbox->Store = StorageKitImpl::GetRoot(); if (!Mailbox->Store && Create) { Mailbox->Store = CreateRoot(Mailbox); } if (Mailbox && Mailbox->Store) { Mailbox->Store->Object = Mailbox; } } return Mailbox; } FolderData *LMail2Store::GetFolder(char *Path) { StorageItem *root = StorageKitImpl::GetRoot(); FolderData *r = CastFolder(root); if (!r) { LAssert(!"No root node."); LgiTrace("%s:%i - Couldn't cast root to folder.\n", _FL); return NULL; } LToken t(Path, "/"); if (t.Length()) { for (unsigned i=0; iLoad(true, false); FolderData *m = 0; r->SubFolders(); for (unsigned n=0; nSub.Length(); n++) { FolderData *c = r->Sub.a[n]; if (c->Name && _stricmp(c->Name, p) == 0) { m = c; break; } } if (m) { r = m; } else { LgiTrace("%s:%i - No match for '%s'.\n", _FL, p); return 0; } } } else LgiTrace("%s:%i - No parts to path.\n", _FL); return r; } FolderData *LMail2Store::CastFolder(StorageItem *i) { if (!i) return 0; if (i->GetType() != MAGIC_FOLDER && i->GetType() != MAGIC_FOLDER_OLD && i->GetParent() != 0) return 0; return (FolderData*)i->Object; } Store3Status LMail2Store::Delete(LArray &Items, bool ToTrash) { if (Items.Length() == 0) { LgiTrace("%s:%i - Nothing to delete.\n", _FL); return Store3Error; } LVariant TrashPath; if (!Callback->GetSystemPath(FOLDER_TRASH, TrashPath)) { LgiTrace("%s:%i - Couldn't get trash folder path.\n", _FL); return Store3Error; } FolderData *Trash = GetFolder(TrashPath.Str()); if (!Trash) { LgiTrace("%s:%i - Couldn't get ptr to '%s' folder.\n", _FL, TrashPath.Str()); return Store3Error; } Store3Status Status = Store3Success; LArray Mv; for (unsigned n=0; n(Items[n]); if (f) { FolderData *Par = f->GetParent(); if (!Par || Par == Trash) { if (!f->Delete()) Status = Store3Error; } else { Mv.Add(f); } } else { ThingData *i = dynamic_cast(Items[n]); if (i) { FolderData *Par = i->GetParent(); if (!Par || Par == Trash) { if (!i->Delete()) Status = Store3Error; } else { Mv.Add(i); } } } } if (Mv.Length()) return Move(Trash, Mv); return Status; } Store3Status LMail2Store::Move(LDataFolderI *NewFolder, LArray &Items) { Store3Status Status = Store3Error; if (NewFolder && NewFolder->GetStore() == this) { FolderData *f = dynamic_cast(NewFolder); if (f) { FolderData *OldPar = 0; for (unsigned n=0; n(Items[n]); if (i) { OldPar = i->GetParent(); if (i->Store && f->Store) { f->Load(false, true); if (f->Store->GetTree()->AttachItem(i->Store, f->Store)) { Status = Store3Success; LAssert(OldPar->Things.IndexOf(i) >= 0); OldPar->Things.Delete(i); if (f->Things.IndexOf(i) < 0) f->Things.Insert(i); else LAssert(!"Already has item."); } } } else { FolderData *moving = dynamic_cast(Items[n]); if (moving) { OldPar = moving->GetParent(); if (moving->Store && f->Store) { if (OldPar == f) { // Already parented to that folder. Status = Store3Success; } else if (f->Store->GetTree()->AttachItem(moving->Store, f->Store)) { // Update the containers... LAssert(OldPar->Sub.IndexOf(moving) >= 0); OldPar->Sub.Delete(moving); if (f->Sub.IndexOf(moving) < 0) f->Sub.Insert(moving); else LAssert(!"Folder already exists in Sub"); Status = Store3Success; } } } } } if (Status && Callback) { Callback->OnMove(NewFolder, OldPar, Items); } } } return Status; } Store3Status LMail2Store::Change(LArray &Items, int PropId, LVariant &Value) { if (Items.Length() == 0) return Store3Success; if (PropId == FIELD_FLAGS) { int32 Flags = Value.CastInt32(); if (Flags & 0x80000000) { // Remove a flag for (unsigned i=0; i(Items[i]); if (m) { m->Flags &= Flags; m->Save(); } else Items.DeleteAt(i--); } } else { // Set a flag for (unsigned i=0; i(Items[i]); if (m) { m->Flags |= Flags; m->Save(); } else Items.DeleteAt(i--); } } if (Callback) Callback->OnChange(Items, PropId); } else LAssert(!"Not impl."); return Store3Error; } class Mail2ProgressAdapter : public Progress { LDataPropI *Props; public: Mail2ProgressAdapter(LDataPropI *p) { Props = p; } void SetDescription(const char *d = 0) { Props->SetStr(Store3UiStatus, d); } void SetLimits(int64 l, int64 h) { Props->SetInt(Store3UiMaxPos, (int)h); } int64 Value() { return Val; } void Value(int64 v) { Val = v; Props->SetInt(Store3UiCurrentPos, (int)v); } bool Cancel() { return Props->GetInt(Store3UiCancel) != 0; } void Cancel(bool i) { Props->SetInt(Store3UiCancel, i); } }; bool LMail2Store::Compact(LViewI *Parent, LDataPropI *Props) { Mail2ProgressAdapter Prog(Props); return StorageKitImpl::Compact(&Prog, Props->GetInt(Store3UiInteractive) != 0); } void LMail2Store::OnEvent(void *Param) { } //////////////////////////////////////////////////////////////////////////////////////////////// LDataStoreI *OpenMail2(char *Mail2Folders, LDataEventsI *Callback, bool Create) { LDataStoreI *s = 0; if (FileExists(Mail2Folders) || Create) { s = new LMail2Store(Mail2Folders, Callback); if (s) { s->GetRoot(Create); } } return s; }