diff --git a/Code/DomTypeValues.h b/Code/DomTypeValues.h --- a/Code/DomTypeValues.h +++ b/Code/DomTypeValues.h @@ -1,245 +1,244 @@ _(Name) _(Quote) _(Home) _(Now) _(Folder) _(AppName) _(Execute) _(BuildType) _(Tag) _(Version) _(Build) _(Platform) _(Language) _(String) _(CurrentFolder) _(View) _(Scribe) _(Mail) _(Contact) _(Filter) _(TestConditions) _(Type) _(ConditionsXml) _(ActionsXml) _(Length) _(MimeType) _(ContentId) _(Data) _(Path) _(Unread) _(Item) _(ItemType) _(Child) _(Next) _(List) _(From) _(FromHtml) _(To) _(ToHtml) _(Subject) _(Body) _(BodyAsText) _(BodyAsHtml) _(HtmlHeadFields) -_(MessageID) +_(MessageId) _(InternetHeaders) _(InternetHeader) _(Priority) _(Html) _(Flags) _(DateSent) _(DateReceived) _(Sig) _(Size) _(Label) _(Attachments) _(Attachment) _(MimeTree) _(Ui) _(Selected) _(Select) _(Read) _(ShowImages) _(Colour) _(CalendarToday) _(InboxSummary) _(Calendar) _(Text) _(Email) _(Groups) _(Title) _(First) _(Surname) _(Nickname) _(Spouse) _(Notes) _(Uid) _(TimeZone) _(HomeStreet) _(HomeSuburb) _(HomePostcode) _(HomeState) _(HomeCountry) _(HomePhone) _(HomeMobile) _(HomeIM) _(HomeFax) _(HomeWebpage) _(WorkStreet) _(WorkSuburb) _(WorkPostcode) _(WorkState) _(WorkCountry) _(WorkPhone) _(WorkMobile) _(WorkIM) _(WorkFax) _(Company) _(WorkWebpage) _(Plugins) _(ReceivedDate) _(SendDate) _(File) _(ImapFlags) _(Attendence) _(Reponse) _(Index) _(Incoming) _(Outgoing) _(Internal) _(Start) _(End) _(Location) _(Reminder) _(Action) _(ActionArg) _(ShowTimeAs) _(Recur) _(RecurFreq) _(RecurInterval) _(FilterDays) _(FilterMonths) _(FilterYears) _(FilterPos) _(RecurEndType) _(RecurEndDate) _(RecurEndCount) _(RecurFilterDays) _(RecurFilterMonths) _(Privacy) _(AttachmentNames) _(EnumLanguages) _(EnumDictionaries) _(CheckWord) _(AddToDict) _(Description) _(Props) _(Dictionary) _(ErrorMessage) _(UserName) _(DebugDelay) _(ClearSign) _(HomeFolder) _(HostName) _(Port) _(Password) _(BaseDn) _(BindDn) _(Recursive) _(Protocol) _(Deref) _(Restart) _(Search) _(UseMoz) _(Image) _(ImageHtml) _(NoContact) _(AddCondition) _(AddAction) _(StopFiltering) _(DoActions) _(Send) _(Receive) _(Identity) _(IsValid) _(IsOnline) _(EndSession) _(Kill) _(Disconnect) _(Stop) _(Accounts) _(CreateAccount) _(DeleteAccount) _(GetUserString) _(HtmlSig) _(OnlySendThis) _(Reply) _(Id) _(Disable) _(AccountExpanded) _(Server) _(Domain) _(Auth) _(AuthType) _(PrefCharset1) _(PrefCharset2) _(AutoReceive) _(CheckEvery) _(LeaveOnServer) _(DeleteAfter) _(DelIfLarger) _(DelIfLargerSize) _(MaxSize) _(8BitCharSet) _(AsciiCharset) _(Install) _(OnBeforeMailSend) _(OnAfterMailReceive) _(OnThingContextMenu) _(OnFolderContextMenu) _(OnThingToolbar) _(OnApplicationToolbar) _(OnBeforeInstallBar) _(OnInstallComponent) _(OnRenderMail) _(OnLoad) _(Undelete) _(Refresh) _(Expunge) _(Reminders) _(LastCheck) _(GrowlOnMail) _(GrowlInfo) _(ContactGroup) _(LookupContactGroup) _(GetClipboardText) _(SetClipboardText) _(AllDay) _(Expanded) _(ShowRemoteContent) _(Options) _(MailStorePaths) _(RootFolders) _(DateModified) _(UsedTs) _(CustomFields) _(OnTimer) _(AddCalendarEvent) _(GetRead) _(SetRead) _(Load) _(Save) _(SetHtml) _(GetTemp) _(Import) _(Export) _(Status) _(Completed) _(BayesianChange) _(BayesianScore) _(ReceivedDomain) _(SearchHtml) _(GetUri) -_(DeleteAsSpam) -_(MessageId) \ No newline at end of file +_(DeleteAsSpam) \ No newline at end of file diff --git a/Code/ScribeMail.cpp b/Code/ScribeMail.cpp --- a/Code/ScribeMail.cpp +++ b/Code/ScribeMail.cpp @@ -1,10255 +1,10264 @@ /* ** 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}, + {"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, 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 + 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(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([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, 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) + { + LAssert(!"No input."); 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)) + auto obj = GetObject(); + if (!obj) + { + LAssert(!"No storage object."); + return false; + } + + if (!obj->SetRfc822(Msg)) { LAssert(!"Failed to set mime content."); return false; } // Now parse them into the meta fields... - GetObject()->ParseHeaders(); + obj->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)) + if (DecodeUuencodedAttachment(obj->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); 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/ScribeUtils.cpp b/Code/ScribeUtils.cpp --- a/Code/ScribeUtils.cpp +++ b/Code/ScribeUtils.cpp @@ -1,2099 +1,2104 @@ #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) { + // If this asserts it's likely you have a duplicate value in Code/DomTypeValues.h #undef _ - #define _(name) Scribe_StrToDom.Add(#name, Sd##name); \ + #define _(name) LAssert(Scribe_StrToDom.Find(#name) == SdNone); \ + Scribe_StrToDom.Add(#name, Sd##name); \ LAssert(Scribe_StrToDom.Find(#name) == Sd##name); \ - Scribe_DomToStr.Add(Sd##name, #name); + \ + LAssert(Scribe_DomToStr.Find(Sd##name) == NULL); \ + Scribe_DomToStr.Add(Sd##name, #name); \ + LAssert(Scribe_DomToStr.Find(Sd##name) != NULL); #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); } diff --git a/Code/Store3Common.cpp b/Code/Store3Common.cpp --- a/Code/Store3Common.cpp +++ b/Code/Store3Common.cpp @@ -1,532 +1,535 @@ #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(), [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; + int Block = 1024; - for (int Sz = 1024; Msg && Sz < (64 << 10); Sz += 1024) + Msg->SetPos(0); + for (ssize_t Pos = 0; Pos < (512 << 10); ) { - Msg->SetPos(0); - if (!s.Length(Sz)) + if (!s.Length(Pos + Block)) break; - ssize_t Rd = Msg->Read(s.Get(), s.Length()); + ssize_t Rd = Msg->Read(s.Get() + Pos, s.Length() - Pos); if (Rd <= 0) { s.Empty(); break; } - s.Length(Rd); + Pos += Rd; + s.Length(Pos); + 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/Store3Imap/ScribeImap_Attachment.cpp b/Code/Store3Imap/ScribeImap_Attachment.cpp --- a/Code/Store3Imap/ScribeImap_Attachment.cpp +++ b/Code/Store3Imap/ScribeImap_Attachment.cpp @@ -1,304 +1,304 @@ #include "lgi/common/Lgi.h" #include "Scribe.h" #include "ScribeImap.h" #include "lgi/common/TextConvert.h" ImapAttachment::ImapAttachment(ImapStore *store, ImapMail *mail, LMime *seg) : Store3Attachment(store) { Mail = mail; InMemoryOnly = false; if ((Seg = seg)) { ImapAttachment *c; for (int i=0; iLength(); i++) { if ((c = new ImapAttachment(Kit, 0, (*Seg)[i]))) { c->AttachTo(this); } } } } ImapAttachment::ImapAttachment(ImapStore *store, ImapMail *mail, LDataPropI *att) : Store3Attachment(store) { Mail = mail; InMemoryOnly = false; if ((Seg = new LMime)) { if (att) { LDataPropI *d = dynamic_cast(att); if (d) CopyProps(*d); else LAssert(!"Not the right object."); } } } ImapAttachment::~ImapAttachment() { // Deleting the seg here means that all the child mime segs will be deleted while // out attachment children still have pointers to them. So we clear the mime seg // pointers owned by our ImapAttachment children first... so that they don't have // dangling pointers. _ClearChildSegs(); DeleteObj(Seg); _Delete(); } void ImapAttachment::SetInMemoryOnly(bool b) { InMemoryOnly = b; } void ImapAttachment::_ClearChildSegs() { for (unsigned i=0; iSeg = 0; c->_ClearChildSegs(); } } void ImapAttachment::OnSave() { if (!Mail) { LAssert(!"Segment is not attached to a mail!"); return; } if (Dirty) { Save(); } for (unsigned i=0; iOnSave(); } } Store3CopyImpl(ImapAttachment) { if (!Seg) Seg = new LMime; auto Hdrs = p.GetStr(FIELD_INTERNET_HEADER); if (Hdrs) { SetStr(FIELD_INTERNET_HEADER, p.GetStr(FIELD_INTERNET_HEADER)); } else { SetStr(FIELD_MIME_TYPE, p.GetStr(FIELD_MIME_TYPE)); SetStr(FIELD_NAME, p.GetStr(FIELD_NAME)); SetStr(FIELD_CONTENT_ID, p.GetStr(FIELD_CONTENT_ID)); SetStr(FIELD_CHARSET, p.GetStr(FIELD_CHARSET)); } LDataI *Data = dynamic_cast(&p); if (Data) { LAutoStreamI tmp = Data->GetStream(_FL); SetStream(tmp); } return true; } uint32_t ImapAttachment::Type() { return MAGIC_ATTACHMENT; } bool ImapAttachment::IsOnDisk() { return Seg != 0; } uint64 ImapAttachment::Size() { int64 Size = 0; if (Seg) { - char *h = Seg->GetHeaders(); + auto h = Seg->GetHeaders(); Size += h ? strlen(h) : 0; LStreamI *s = Seg->GetData(); if (s) { int64 sz = s->GetSize(); if (sz > 0) Size += sz; } } return Size; } const char *ImapAttachment::GetStr(int id) { switch (id) { case FIELD_NAME: { if (!Name) { Name.Reset(DecodeRfc2047(Seg->GetSub("Content-Type", "name"))); if (!Name) Name.Reset(DecodeRfc2047(Seg->GetSub("Content-Disposition", "filename"))); } return Name; break; } case FIELD_MIME_TYPE: { if (!MimeType) MimeType.Reset(Seg->GetMimeType()); return MimeType; break; } case FIELD_CONTENT_ID: { if (!ContentId) ContentId.Reset(Seg->Get("Content-Id")); return ContentId; break; } case FIELD_CHARSET: { if (!Charset) { // Check the headers... Charset.Reset(Seg->GetSub("Content-Type", "charset")); if (!Charset) { // Maybe a parent segment has a charset? for (ImapAttachment *p = GetParent(); p; p = p->GetParent()) { auto Cs = p->GetStr(FIELD_CHARSET); if (Cs) return Cs; } } } return Charset; break; } case FIELD_INTERNET_HEADER: { return Seg->GetHeaders(); break; } } LAssert(0); return 0; } Store3Status ImapAttachment::SetStr(int id, const char *str) { LAssert(Seg != NULL); if (!Seg) return Store3Error; switch (id) { case FIELD_INTERNET_HEADER: { Seg->SetHeaders(str); break; } case FIELD_NAME: { Seg->SetSub("Content-Type", "name", str, "application/octet"); break; } case FIELD_MIME_TYPE: { Seg->SetMimeType(str); break; } case FIELD_CONTENT_ID: { Seg->Set("Content-Id", str); break; } case FIELD_CHARSET: { Seg->SetSub("Content-Type", "charset", str); break; } default: LAssert(0); return Store3Error; } return Store3Success; } int64 ImapAttachment::GetInt(int id) { switch (id) { case FIELD_SIZE: { return Seg ? Seg->GetLength() : 0; break; } case FIELD_STORE_TYPE: { return Store3Imap; } } LAssert(0); return -1; } Store3Status ImapAttachment::SetInt(int id, int64 i) { switch (id) { case FIELD_SIZE: { break; } } return Store3Error; } Store3Status ImapAttachment::Save(LDataI *Folder) { LAssert(0); return Store3Error; } Store3Status ImapAttachment::Delete(bool ToTrash) { if (Mail) Mail->SegDirty = true; _Delete(); return Store3Success; } LAutoStreamI ImapAttachment::GetStream(const char *file, int line) { LStreamI *s = Seg->GetData(); // int64 size = s->GetSize(); // int64 pos = s->GetPos(); return LAutoStreamI(s && s->GetSize() > 0 ? new LProxyStream(s) : 0); } bool ImapAttachment::SetStream(LAutoStreamI stream) { return Seg->SetData(true, new LMemStream(stream, 0, -1)); } diff --git a/Code/Store3Mail3/Mail3.h b/Code/Store3Mail3/Mail3.h --- a/Code/Store3Mail3/Mail3.h +++ b/Code/Store3Mail3/Mail3.h @@ -1,779 +1,779 @@ #ifndef _MAIL3_H_ #define _MAIL3_H_ #include "lgi/common/Lgi.h" #include "Store3Common.h" #include "v3.6.14/sqlite3.h" // Debugging stuff #define MAIL3_TRACK_OBJS 0 #define MAIL3_DB_FILE "Database.sqlite" #define MAIL3_TBL_FOLDER "Folder" #define MAIL3_TBL_FOLDER_FLDS "FolderFields" #define MAIL3_TBL_MAIL "Mail" #define MAIL3_TBL_MAILSEGS "MailSegs" #define MAIL3_TBL_CONTACT "Contact" #define MAIL3_TBL_GROUP "ContactGroup" #define MAIL3_TBL_FILTER "Filter" #define MAIL3_TBL_CALENDAR "Calendar" class LMail3Store; class LMail3Mail; enum Mail3SubFormat { Mail3v1, // All the mail and segs in a single tables. Mail3v2, // All the mail and segs in per folders tables. }; struct GMail3Def { const char *Name; const char *Type; }; struct GMail3Idx { const char *IdxName; const char *Table; const char *Column; }; class Mail3BlobStream : public LStream { LMail3Store *Store; sqlite3_blob *b; int64 Pos, Size; int SegId; bool WriteAccess; bool OpenBlob(); bool CloseBlob(); public: const char *File; int Line; Mail3BlobStream(LMail3Store *Store, int segId, int size, const char *file, int line, bool Write = false); ~Mail3BlobStream(); int64 GetPos(); int64 SetPos(int64 p); int64 GetSize(); int64 SetSize(int64 sz); ssize_t Read(void *Buf, ssize_t Len, int Flags = 0); ssize_t Write(const void *Buf, ssize_t Len, int Flags = 0); }; extern GMail3Def TblFolder[]; extern GMail3Def TblFolderFlds[]; extern GMail3Def TblMail[]; extern GMail3Def TblMailSegs[]; extern GMail3Def TblContact[]; extern GMail3Def TblFilter[]; extern GMail3Def TblGroup[]; extern GMail3Def TblCalendar[]; #define SERIALIZE_STR(Var, Col) \ if (Write) \ { \ if (!s.SetStr(Col, Var.Str())) \ return false; \ } \ else \ Var = s.GetStr(Col); #define SERIALIZE_DATE(Var, Col) \ if (Write) \ { \ if (Var.IsValid()) Var.ToUtc(); \ if (!s.SetDate(Col, Var)) \ return false; \ } \ else \ { \ int Fmt = Var.GetFormat(); \ Var.SetFormat(GDTF_YEAR_MONTH_DAY|GDTF_24HOUR); \ Var.Set(s.GetStr(Col)); \ Var.SetFormat(Fmt); \ Var.SetTimeZone(0, false); \ } #define SERIALIZE_BOOL(Var, Col) \ if (Write) \ { \ if (!s.SetInt(Col, Var)) \ return false; \ } \ else \ Var = s.GetBool(Col); #define SERIALIZE_INT(Var, Col) \ if (Write) \ { \ if (!s.SetInt(Col, Var)) \ return false; \ } \ else \ Var = s.GetInt(Col); #define SERIALIZE_INT64(Var, Col) \ if (Write) \ { \ if (!s.SetInt64(Col, Var)) \ return false; \ } \ else \ Var = s.GetInt64(Col); #define SERIALIZE_COLOUR(Colour, Col) \ if (Write) \ { \ int64_t c = Colour.IsValid() ? Colour.c32() : 0; \ if (!s.SetInt64(Col, c)) \ return false; \ } \ else \ { \ int64_t c = s.GetInt64(Col); \ if (c > 0) Colour.c32((uint32_t)c); \ else Colour.Empty(); \ } #define SERIALIZE_AUTOSTR(var, Col) \ { \ if (Write) \ { \ if (!s.SetStr(Col, var)) \ return false; \ } \ else \ { \ var.Reset(NewStr(s.GetStr(Col))); \ } \ } #define SERIALIZE_LSTR(var, Col) \ { \ if (Write) \ { \ if (!s.SetStr(Col, var)) \ return false; \ } \ else \ { \ var = s.GetStr(Col); \ } \ } struct LMail3StoreMsg { enum Type { MsgNone, MsgCompactComplete, MsgRepairComplete, } Msg; int64_t Int; LString Str; LMail3StoreMsg(Type type) { Msg = type; } }; class LMail3Store : public LDataStoreI { friend class LMail3Obj; friend class LMail3Mail; friend class Mail3Trans; friend class CompactThread; LDataEventsI *Callback; sqlite3 *Db; LString Folder; LString DbFile; class LMail3Folder *Root; LHashTbl, GMail3Def*> Fields; LHashTbl, GMail3Idx*> Indexes; Store3Status OpenStatus; LHashTbl, Store3Status> TableStatus; LString ErrorMsg; LString StatusMsg; LString TempPath; std::function CompactOnStatus; std::function RepairOnStatus; struct TableDefn : LArray { LString::Array t; }; bool ParseTableFormat(const char *Name, TableDefn &Defs); Store3Status CheckTable(const char *Name, GMail3Def *Flds); bool UpdateTable(const char *Name, GMail3Def *Flds, Store3Status Check); bool DeleteMailById(int64 Id); bool OpenDb(); bool CloseDb(); LMail3Folder *GetSystemFolder(int Type); public: Mail3SubFormat Format; class LStatement { struct PostBlob { int64 Size; LVariant ColName; LStreamI *Data; }; LArray Post; protected: LMail3Store *Store; sqlite3_stmt *s; LVariant Table; LAutoString TempSql; public: LStatement(LMail3Store *store, const char *sql = 0); virtual ~LStatement(); operator sqlite3_stmt *() { return s; } bool IsOk() { return #ifndef __llvm__ this != 0 && #endif Store != 0 && s != 0; } bool Prepare(const char *Sql); bool Row(); bool Exec(); bool Reset(); bool Finalize(); int64 LastInsertId(); int GetSize(int Col); bool GetBool(int Col); int GetInt(int Col); bool SetInt(int Col, int n); int64 GetInt64(int Col); bool SetInt64(int Col, int64 n); char *GetStr(int Col); bool GetBinary(int Col, LVariant *v); - bool SetStr(int Col, char *s); + bool SetStr(int Col, const char *s); bool SetDate(int Col, LDateTime &d); bool SetStream(int Col, const char *ColName, LStreamI *s); bool SetBinary(int Col, const char *ColName, LVariant *v); virtual int64 GetRowId() { LAssert(0); return -1; } }; class LInsert : public LStatement { public: LInsert(LMail3Store *store, const char *Tbl); int64 GetRowId() { return LastInsertId(); } }; class LUpdate : public LStatement { int64 RowId; public: LUpdate(LMail3Store *store, const char *Tbl, int64 rowId, char *ExcludeField = 0); int64 GetRowId() { return RowId; } }; class LTransaction { LMail3Store *Store; bool Open; public: LTransaction(LMail3Store *store); ~LTransaction(); bool RollBack(); }; #if MAIL3_TRACK_OBJS struct SqliteObjs { LStatement *Stat; Mail3BlobStream *Stream; SqliteObjs() { Stat = 0; Stream = 0; } }; LArray All; template bool RemoveFromAll(T *p) { for (int i=0; i &Items); Store3Status Delete(LArray &Items, bool ToTrash); Store3Status Change(LArray &Items, int PropId, LVariant &Value, LOperator Operator); void Compact(LViewI *Parent, LDataPropI *Props, std::function OnStatus); void Upgrade(LViewI *Parent, LDataPropI *Props, std::function OnStatus); void Repair(LViewI *Parent, LDataPropI *Props, std::function OnStatus); bool SetFormat(LViewI *Parent, LDataPropI *Props); void PostStore(LMail3StoreMsg *m) { Callback->Post(this, m); } void OnEvent(void *Param); bool Check(int Code, const char *Sql); GMail3Def *GetFields(const char *t) { return Fields.Find(t); } StoreTrans StartTransaction(); // LDataEventsI wrappers void OnNew(const char *File, int Line, LDataFolderI *parent, LArray &new_items, int pos, bool is_new); bool OnMove(const char *File, int Line, LDataFolderI *new_parent, LDataFolderI *old_parent, LArray &items); bool OnChange(const char *File, int Line, LArray &items, int FieldHint); bool OnDelete(const char *File, int Line, LDataFolderI *parent, LArray &items); private: class Mail3Trans : public LDataStoreI::LDsTransaction { Mail3Trans **Ptr; LTransaction Trans; public: Mail3Trans(LMail3Store *s, Mail3Trans **ptr); ~Mail3Trans(); } *Transaction; }; class LMail3Obj { protected: bool Check(int r, char *sql); public: LMail3Store *Store; int64 Id; int64 ParentId; LMail3Obj(LMail3Store *store) { Id = ParentId = -1; Store = store; } bool Write(const char *Table, bool Insert); virtual const char *GetClass() { return "LMail3Obj"; } virtual bool Serialize(LMail3Store::LStatement &s, bool Write) = 0; virtual void SetStore(LMail3Store *s) { Store = s; } }; class LMail3Thing : public LDataI, public LMail3Obj { friend class LMail3Store; LMail3Thing &operator =(LMail3Thing &p) = delete; protected: bool NewMail = false; virtual const char *GetTable() { LAssert(0); return 0; } virtual void OnSave() {}; public: LMail3Folder *Parent = NULL; LMail3Thing(LMail3Store *store) : LMail3Obj(store) { } ~LMail3Thing(); const char *GetClass() { return "LMail3Thing"; } bool IsOnDisk() { return Id > 0; } bool IsOrphan() { return false; } uint64 Size() { return sizeof(*this); } uint32_t Type() { LAssert(0); return 0; } Store3Status Delete(bool ToTrash); LDataStoreI *GetStore() { return Store; } LAutoStreamI GetStream(const char *file, int line) { LAssert(0); return LAutoStreamI(0); } bool Serialize(LMail3Store::LStatement &s, bool Write) { LAssert(0); return false; } Store3Status Save(LDataI *Folder = 0); virtual bool DbDelete() { LAssert(0); return false; } }; class LMail3Folder : public LDataFolderI, public LMail3Obj { public: LVariant Name; int Unread; int Open; int ItemType; int Sort; // Which field to sort contents on int Threaded; int SiblingIndex; // The index of this folder when sorting amongst other sibling folders union { int AccessPerms; struct { int16_t ReadPerm; int16_t WritePerm; }; }; Store3SystemFolder System; LMail3Folder *Parent; DIterator Sub; DIterator Items; DIterator Flds; LMail3Folder(LMail3Store *store); ~LMail3Folder(); Store3CopyDecl; bool Serialize(LMail3Store::LStatement &s, bool Write) override; const char *GetClass() override { return "LMail3Folder"; } uint32_t Type() override; bool IsOnDisk() override; bool IsOrphan() override; uint64 Size() override; Store3Status Save(LDataI *Folder) override; Store3Status Delete(bool ToTrash) override; LDataStoreI *GetStore() override; LAutoStreamI GetStream(const char *file, int line) override; bool SetStream(LAutoStreamI stream) override; LDataIterator &SubFolders() override; LDataIterator &Children() override; LDataIterator &Fields() override; Store3Status DeleteAllChildren() override; Store3Status FreeChildren() override; LMail3Folder *FindSub(char *Name); bool DbDelete(); bool GenSizes(); const char *GetStr(int id) override; Store3Status SetStr(int id, const char *str) override; int64 GetInt(int id) override; Store3Status SetInt(int id, int64 i) override; }; class LMail3Attachment : public Store3Attachment { int64 SegId; int64 BlobSize; LAutoString Headers; LString Name; LString MimeType; LString ContentId; LString Charset; /// This is set when the segment is not to be stored on disk. /// When signed and/or encrypted messages are stored, the original /// rfc822 image is maintained by not MIME decoding into separate /// segments but leaving it MIME encoded in one seg (headers and body). /// At runtime the segment is loaded and parsed into a temporary tree /// of LMail3Attachment objects. This flag is set for those temporary /// nodes. bool InMemoryOnly; public: LMail3Attachment(LMail3Store *store); ~LMail3Attachment(); void SetInMemoryOnly(bool b); int64 GetId() { return SegId; } LMail3Attachment *Find(int64 Id); bool Load(LMail3Store::LStatement &s, int64 &ParentId); bool ParseHeaders() override; char *GetHeaders(); bool HasHeaders() { return ValidStr(Headers); } Store3CopyDecl; const char *GetStr(int id) override; Store3Status SetStr(int id, const char *str) override; int64 GetInt(int id) override; Store3Status SetInt(int id, int64 i) override; Store3Status Delete(bool ToTrash = false) override; LAutoStreamI GetStream(const char *file, int line) override; bool SetStream(LAutoStreamI stream) override; uint32_t Type() override { return MAGIC_ATTACHMENT; } bool IsOnDisk() override { return SegId > 0; } bool IsOrphan() override { return false; } uint64 Size() override; uint64 SizeChildren(); Store3Status Save(LDataI *Folder = 0) override; void OnSave() override; }; class LMail3Mail : public LMail3Thing { LAutoString TextCache; LAutoString HtmlCache; LString IdCache; LAutoPtr SizeCache; LString InferredCharset; void LoadSegs(); void OnSave() override; const char *GetTable() override { return MAIL3_TBL_MAIL; } void ParseAddresses(char *Str, int CC); const char *InferCharset(); bool Utf8Check(LVariant &v); bool Utf8Check(LAutoString &v); public: int Priority = MAIL_PRIORITY_NORMAL; int Flags = 0; int AccountId = 0; int64 MailSize = 0; uint32_t MarkColour = Rgba32(0, 0, 0, 0); // FIELD_MARK_COLOUR, 32bit rgba colour LVariant Subject; DIterator To; LMail3Attachment *Seg = NULL; Store3Addr From; Store3Addr Reply; LVariant Label; LVariant MessageID; LVariant References; LVariant FwdMsgId; LVariant BounceMsgId; LVariant ServerUid; LDateTime DateReceived; LDateTime DateSent; LMail3Mail(LMail3Store *store); ~LMail3Mail(); Store3CopyDecl; void SetStore(LMail3Store *s) override; bool Serialize(LMail3Store::LStatement &s, bool Write) override; const char *GetClass() override { return "LMail3Mail"; } LMail3Attachment *GetAttachment(int64 Id); bool FindSegs(const char *MimeType, LArray &Segs, bool Create = false); int GetAttachments(LArray *Lst = 0); bool ParseHeaders() override; void ResetCaches(); uint32_t Type() override; uint64 Size() override; bool DbDelete() override; LDataStoreI *GetStore() override; LAutoStreamI GetStream(const char *file, int line) override; bool SetStream(LAutoStreamI stream) override; const char *GetStr(int id) override; Store3Status SetStr(int id, const char *str) override; int64 GetInt(int id) override; Store3Status SetInt(int id, int64 i) override; const LDateTime *GetDate(int id) override; Store3Status SetDate(int id, const LDateTime *t) override; LDataPropI *GetObj(int id) override; Store3Status SetObj(int id, LDataPropI *i) override; LDataIt GetList(int id) override; Store3Status SetRfc822(LStreamI *m) override; }; class LMail3Contact : public LMail3Thing { const char *GetTable() override { return MAIL3_TBL_CONTACT; } LHashTbl, LString*> f; public: LVariant Image; LDateTime DateMod; LMail3Contact(LMail3Store *store); ~LMail3Contact(); uint32_t Type() override { return MAGIC_CONTACT; } bool Serialize(LMail3Store::LStatement &s, bool Write) override; Store3CopyDecl; const char *GetClass() override { return "LMail3Contact"; } bool DbDelete() override; const char *GetStr(int id) override; Store3Status SetStr(int id, const char *str) override; int64 GetInt(int id) override { return -1; } LVariant *GetVar(int id) override; Store3Status SetVar(int id, LVariant *i) override; const LDateTime *GetDate(int id) override; Store3Status SetDate(int id, const LDateTime *i) override; }; class LMail3Group : public LMail3Thing { const char *GetTable() override { return MAIL3_TBL_GROUP; } LString Name; LString Group; LDateTime DateMod; public: LMail3Group(LMail3Store *store); ~LMail3Group(); uint32_t Type() override { return MAGIC_GROUP; } bool Serialize(LMail3Store::LStatement &s, bool Write) override; Store3CopyDecl; const char *GetClass() override { return "LMail3Group"; } bool DbDelete() override; const char *GetStr(int id) override; Store3Status SetStr(int id, const char *str) override; int64 GetInt(int id) override { return -1; } const LDateTime *GetDate(int id) override; Store3Status SetDate(int id, const LDateTime *i) override; }; class LMail3Filter : public LMail3Thing { int Index; int StopFiltering; int Direction; LString Name; LString ConditionsXml; LString ActionsXml; LString Script; LDateTime Modified; const char *GetTable() override { return MAIL3_TBL_FILTER; } public: LMail3Filter(LMail3Store *store); ~LMail3Filter(); uint32_t Type() override { return MAGIC_FILTER; } LDataStoreI *GetStore() override { return Store; } bool Serialize(LMail3Store::LStatement &s, bool Write) override; Store3CopyDecl; const char *GetClass() override { return "LMail3Filter"; } bool DbDelete() override; const char *GetStr(int id) override; Store3Status SetStr(int id, const char *str) override; int64 GetInt(int id) override; Store3Status SetInt(int Col, int64 n) override; const LDateTime *GetDate(int id) override; Store3Status SetDate(int id, const LDateTime *i) override; }; class LMail3Calendar : public LMail3Thing { LString ToCache; private: int CalType; // FIELD_CAL_TYPE LString To; // FIELD_TO CalendarPrivacyType CalPriv; // FIELD_CAL_PRIVACY int Completed; // FIELD_CAL_COMPLETED LDateTime Start; // FIELD_CAL_START_UTC LDateTime End; // FIELD_CAL_END_UTC LString TimeZone; // FIELD_CAL_TIMEZONE LString Subject; // FIELD_CAL_SUBJECT LString Location; // FIELD_CAL_LOCATION LString Uid; // FIELD_UID bool AllDay; // FIELD_CAL_ALL_DAY int64 StoreStatus; // FIELD_STATUS - ie the current Store3Status LString EventStatus; // FIELD_CAL_STATUS LDateTime Modified; // FIELD_DATE_MODIFIED LString Reminders; // FIELD_CAL_REMINDERS LDateTime LastCheck; // FIELD_CAL_LAST_CHECK int ShowTimeAs; // FIELD_CAL_SHOW_TIME_AS int Recur; // FIELD_CAL_RECUR int RecurFreq; // FIELD_CAL_RECUR_FREQ int RecurInterval; // FIELD_CAL_RECUR_INTERVAL LDateTime RecurEnd; // FIELD_CAL_RECUR_END_DATE int RecurCount; // FIELD_CAL_RECUR_END_COUNT int RecurEndType; // FIELD_CAL_RECUR_END_TYPE LString RecurPos; // FIELD_CAL_RECUR_FILTER_POS int FilterDays; // FIELD_CAL_RECUR_FILTER_DAYS int FilterMonths; // FIELD_CAL_RECUR_FILTER_MONTHS LString FilterYears; // FIELD_CAL_RECUR_FILTER_YEARS LString Notes; // FIELD_CAL_NOTES LColour Colour; const char *GetTable() override { return MAIL3_TBL_CALENDAR; } public: LMail3Calendar(LMail3Store *store); ~LMail3Calendar(); uint32_t Type() override { return MAGIC_CALENDAR; } LDataStoreI *GetStore() override { return Store; } bool Serialize(LMail3Store::LStatement &s, bool Write) override; Store3CopyDecl; const char *GetClass() override { return "LMail3Filter"; } bool DbDelete() override; const char *GetStr(int id) override; Store3Status SetStr(int id, const char *str) override; int64 GetInt(int id) override; Store3Status SetInt(int Col, int64 n) override; const LDateTime *GetDate(int id) override; Store3Status SetDate(int id, const LDateTime *t) override; }; #endif diff --git a/Code/Store3Mail3/Mail3Store.cpp b/Code/Store3Mail3/Mail3Store.cpp --- a/Code/Store3Mail3/Mail3Store.cpp +++ b/Code/Store3Mail3/Mail3Store.cpp @@ -1,2227 +1,2229 @@ #include "Mail3.h" #include "resdefs.h" #include "lgi/common/SubProcess.h" #include #include "lgi/common/LgiRes.h" #include "lgi/common/Thread.h" #include "lgi/common/Store3MimeTree.h" #define SanityCheck() if (!IsOk()) return NULL; /////////////////////////////////////////////////////////////////////////////////// LMail3Thing::~LMail3Thing() { if (Parent) Parent->Items.a.Delete(this); } Store3Status LMail3Thing::Delete(bool ToTrash) { LArray Del; Del.Add(this); return Store->Delete(Del, ToTrash); } Store3Status LMail3Thing::Save(LDataI *Folder) { LMail3Folder *Fld = dynamic_cast(Folder); if (Folder && !Fld) { LAssert(!"Not the right folder type"); LgiTrace("%s:%i - Not the right folder type.\n", _FL); return Store3Error; } if (Fld && Fld->Store != Store) { SetStore(Fld->Store); } const char *Table = GetTable(); bool Create = false; bool IsNewMail = Type() == MAGIC_MAIL && TestFlag(GetInt(FIELD_FLAGS), MAIL_NEW); LArray NewItems; if (Id < 0) { if (Fld) { // Make sure the folder is loaded... Fld->Children(); Parent = Fld; ParentId = Fld->Id; LAssert(Parent->Items.IndexOf(this, true) < 0); // LgiTrace("%s:%i - Saving %i to %s (%i items)\n", _FL, (int)Id, Parent->GetStr(FIELD_FOLDER_NAME), Parent->Items.Length()); Parent->Items.Insert(this, -1, true); Create = true; NewItems.Add(this); if (IsNewMail) SetInt(FIELD_FLAGS, GetInt(FIELD_FLAGS) & ~MAIL_NEW); } else { LAssert(!"No parent to save to."); LgiTrace("%s:%i - No parent to save to.\n", _FL); return Store3Error; } } else if (Fld) { // Make sure the folder is loaded... Fld->Children(); ParentId = Fld->Id; if (Parent != Fld) { LDataFolderI *Old = Parent; if (Parent) { // Remove this item from the current parent folder... LAssert(Parent->Items.IndexOf(this) >= 0); Parent->Items.Delete(this); } // Add it to the new one... Parent = Fld; LAssert(Parent->Items.IndexOf(this, true) < 0); // LgiTrace("%s:%i - Saving %i to %s (%i items)\n", _FL, (int)Id, Parent->GetStr(FIELD_FOLDER_NAME), Parent->Items.Length()); Parent->Items.Insert(this, -1, true); LArray a; a.Add(this); if (Old) Store->OnMove(_FL, Parent, Old, a); else NewItems.Add(this); if (IsNewMail) SetInt(FIELD_FLAGS, GetInt(FIELD_FLAGS) & ~MAIL_NEW); } } bool Status = Write(Table, Create); if (Status) { OnSave(); if (NewItems.Length()) Store->OnNew(_FL, Parent, NewItems, -1, IsNewMail); } else { LgiTrace("%s:%i - Write failed.\n", _FL); } return Status ? Store3Success : Store3Error; } /////////////////////////////////////////////////////////////////////////////////// GMail3Idx Mail3Indexes[] = { {"MailFolderIdx", MAIL3_TBL_MAIL, "ParentId"}, {"MailSegMailIdx", MAIL3_TBL_MAILSEGS, "MailId"}, {"MailSegParentIdx", MAIL3_TBL_MAILSEGS, "ParentId"}, }; LMail3Store::LMail3Store(const char *Mail3Folder, LDataEventsI *callback, bool Create) : TableStatus(0, Store3Error) { Format = Mail3v1; Folder = Mail3Folder; Callback = callback; Db = 0; Transaction = 0; Root = 0; OpenStatus = Store3Success; Fields.Add(MAIL3_TBL_FOLDER, TblFolder); Fields.Add(MAIL3_TBL_FOLDER_FLDS, TblFolderFlds); Fields.Add(MAIL3_TBL_MAIL, TblMail); Fields.Add(MAIL3_TBL_MAILSEGS, TblMailSegs); Fields.Add(MAIL3_TBL_CONTACT, TblContact); Fields.Add(MAIL3_TBL_GROUP, TblGroup); Fields.Add(MAIL3_TBL_FILTER, TblFilter); Fields.Add(MAIL3_TBL_CALENDAR, TblCalendar); bool Exist = LDirExists(Mail3Folder); if (!Create && !Exist) { ErrorMsg.Printf(LLoadString(IDS_ERROR_FILE_DOESNT_EXIST), Mail3Folder); } else if (!Exist && !FileDev->CreateFolder(Mail3Folder)) { ErrorMsg.Printf(LLoadString(IDS_ERROR_CANT_CREATE_FOLDER), Mail3Folder); } else { OpenDb(); } #if _DEBUG LMail3Mail m(this); Store3MimeTree Tree(&m, m.Seg); if (!Tree.UnitTests(this, &m)) { LgiTrace("%s:%i - Error: Store3MimeTree unit tests failed\n", _FL); LAssert(!"Store3MimeTree unit tests failed.\n"); } #endif } LMail3Store::~LMail3Store() { CloseDb(); DeleteObj(Root); } LMail3Folder *LMail3Store::GetSystemFolder(int Type) { if (!Callback || !Root) return NULL; LVariant Path; if (Callback->GetSystemPath(FOLDER_INBOX, Path)) { auto p = LString(Path.Str()).Split("/"); for (auto f: Root->Sub.a) { auto Nm = f->GetStr(FIELD_FOLDER_NAME); if (p.Last().Equals(Nm)) { return f; } } } return NULL; } LDataPropI *LMail3Store::GetObj(int id) { switch (id) { case FIELD_INBOX: return GetSystemFolder(FOLDER_INBOX); case FIELD_OUTBOX: return GetSystemFolder(FOLDER_OUTBOX); case FIELD_SENT: return GetSystemFolder(FOLDER_SENT); case FIELD_TRASH: return GetSystemFolder(FOLDER_TRASH); } return NULL; } bool LMail3Store::OpenDb() { char f[MAX_PATH_LEN]; StatusMsg.Empty(); LMakePath(f, sizeof(f), Folder, MAIL3_DB_FILE); DbFile = f; if (!Check(sqlite3_open(f, &Db), 0)) { ErrorMsg.Printf(LLoadString(IDS_ERROR_CANT_CREATE_DB), f); return false; } else { // const char *Tbl; // for (GMail3Def *Flds=Fields.First(&Tbl); Flds; Flds=Fields.Next(&Tbl)) for (auto it : Fields) { Store3Status s = CheckTable(it.key, it.value); if (s == Store3Missing && UpdateTable(it.key, it.value, s)) { s = Store3Success; } TableStatus.Add(it.key, s); if (s == Store3Error || (s == Store3UpgradeRequired && OpenStatus != Store3Error)) OpenStatus = s; } LArray DelNames; { LStatement Tables( this, "SELECT name FROM sqlite_master " "WHERE type='table' " "ORDER BY name;"); while (Tables.Row()) { char *Name = Tables.GetStr(0); if (Name && strchr(Name, '_') && !stristr(Name, "sqlite")) DelNames.New().Reset(NewStr(Name)); } } for (unsigned i=0; i &new_items, int pos, bool is_new) { if (!Callback) return; Callback->SetContext(File, Line); Callback->OnNew(parent, new_items, pos, is_new); } bool LMail3Store::OnChange(const char *File, int Line, LArray &items, int FieldHint) { if (!Callback) return false; Callback->SetContext(File, Line); return Callback->OnChange(items, FieldHint); } bool LMail3Store::OnMove(const char *File, int Line, LDataFolderI *new_parent, LDataFolderI *old_parent, LArray &items) { if (!Callback) return false; Callback->SetContext(File, Line); return Callback->OnMove(new_parent, old_parent, items); } bool LMail3Store::OnDelete(const char *File, int Line, LDataFolderI *parent, LArray &items) { if (!Callback) return false; Callback->SetContext(File, Line); return Callback->OnDelete(parent, items); } const char *LMail3Store::GetStr(int id) { switch (id) { case FIELD_ERROR: return ErrorMsg; case FIELD_STATUS: return StatusMsg; case FIELD_TEMP_PATH: return TempPath; case FIELD_STORE_TYPE: return "LMail3Store"; } return NULL; } Store3Status LMail3Store::SetStr(int id, const char *s) { switch (id) { case FIELD_ERROR: ErrorMsg = s; break; case FIELD_STATUS: StatusMsg = s; break; case FIELD_TEMP_PATH: TempPath = s; break; default: LAssert(0); return Store3Error; } return Store3Success; } int64 LMail3Store::GetInt(int id) { switch (id) { case FIELD_STATUS: { if (OpenStatus != Store3Success) return OpenStatus; return IsOk() ? Store3Success : Store3Error; } case FIELD_READONLY: return false; case FIELD_VERSION: return 3; case FIELD_FORMAT: return Format; case FIELD_STORE_TYPE: return Store3Sqlite; } return -1; } Store3Status LMail3Store::SetInt(int id, int64 i) { switch (id) { case FIELD_FORMAT: break; } return Store3Error; } bool LMail3Store::Check(int Code, const char *Sql) { if (Code == SQLITE_OK || Code == SQLITE_DONE) return true; const char *Err = sqlite3_errmsg(Db); LgiTrace("%s:%i - Sqlite error %i: %s\n%s", _FL, Code, Err, Sql?Sql:(char*)"", Sql?"\n":""); LAssert(!"Db Error"); #if MAIL3_TRACK_OBJS for (int i=0; iItems.State = Store3Loaded; Root->Serialize(s, false); } } else { // Create new root folder... if ((Root = new LMail3Folder(this))) { Root->Items.State = Store3Loaded; Root->Write(MAIL3_TBL_FOLDER, true); } } } } return Root; } #define PROFILE_MOVE 0 #if PROFILE_MOVE #define PROF_MOVE(...) prof.Add(__VA_ARGS__) #else #define PROF_MOVE(...) #endif Store3Status LMail3Store::Move(LDataFolderI *NewFolder, LArray &Items) { Store3Status Status = Store3Error; LMail3Folder *To = dynamic_cast(NewFolder); if (!To) return Status; if (Items.Length() == 0) return Store3Success; #if PROFILE_MOVE LProfile prof("LMail3Store::Move"); #endif LDataFolderI *OldParent = NULL; LArray Moved; StoreTrans Tr = StartTransaction(); for (unsigned n=0; n(Items[n]); if (Fld) { PROF_MOVE("0"); if (Fld->ParentId == To->Id) Status = Store3Success; else { char s[256]; sprintf_s(s, sizeof(s), "update " MAIL3_TBL_FOLDER " set ParentId=" LPrintfInt64 " where Id=" LPrintfInt64, To->Id, Fld->Id); PROF_MOVE("1"); LStatement Stmt(this, s); if (Stmt.Exec()) { PROF_MOVE("2"); Status = Store3Success; LAssert(Fld->Parent->Sub.IndexOf(Fld) >= 0); Fld->Parent->Sub.Delete(Fld); LDataFolderI *From = Fld->Parent; Fld->Parent = To; Fld->ParentId = Fld->Parent->Id; To->Sub.Insert(Fld); if (!OldParent) OldParent = From; if (Callback && OldParent && Moved.Length() && OldParent != From) { PROF_MOVE("3"); Callback->OnMove(To, OldParent, Moved); PROF_MOVE("4"); Moved.Length(0); OldParent = From; } Moved.Add(Fld); } } } else if ((Thing = dynamic_cast(Items[n]))) { PROF_MOVE("5"); if (Thing->ParentId == To->Id) Status = Store3Success; else if (To->ItemType != MAGIC_ANY && Thing->Type() != To->ItemType) { LgiTrace("%s:%i - Can't move item (type=%x) to folder containing type %x.\n", _FL, Thing->Type(), To->ItemType); Status = Store3Error; } else { // This needs to be before the SQL update so that duplicate Mail // objects don't get created when moving to an unloaded folder. if (To->Items.GetState() != Store3Loaded) To->Children(); PROF_MOVE("6"); char s[256]; sprintf_s(s, sizeof(s), "update %s set ParentId=" LPrintfInt64 " where Id=" LPrintfInt64, Thing->GetTable(), To->Id, Thing->Id); LStatement Stmt(this, s); if (Stmt.Exec()) { PROF_MOVE("6"); Status = Store3Success; LDataFolderI *From = Thing->Parent; if (Thing->Parent) { LAssert(Thing->Parent->Items.IndexOf(Thing) >= 0); Thing->Parent->Items.Delete(Thing); } Thing->Parent = To; Thing->ParentId = To->Id; LAssert(To->Items.IndexOf(Thing) < 0); #ifdef _DEBUG if (To->System != Store3SystemTrash) { // Check there is no duplicate ID.. for (unsigned k=0; kItems.a.Length(); k++) { if (To->Items.a[k]->Id == Thing->Id) { LAssert(!"Can't have duplicate IDs in the same folder."); } } } #endif // LgiTrace("%s:%i - Saving %p:%i to %s (%i items)\n", _FL, Thing, (int)Thing->Id, To->GetStr(FIELD_FOLDER_NAME), To->Items.Length()); To->Items.Insert(Thing); if (!OldParent) OldParent = From; if (Callback && OldParent && Moved.Length() && OldParent != From) { PROF_MOVE("7"); Callback->OnMove(To, OldParent, Moved); PROF_MOVE("8"); Moved.Length(0); OldParent = From; } Moved.Add(Thing); } } } } if (Callback && Moved.Length()) Callback->OnMove(To, OldParent, Moved); return Status; } Store3Status LMail3Store::Delete(LArray &Items, bool ToTrash) { if (Items.Length() == 0) return Store3Error; LVariant FolderName; LMail3Folder *Trash = 0; if (ToTrash && Callback->GetSystemPath(FOLDER_TRASH, FolderName)) { auto t = LString(FolderName.Str()).SplitDelimit("/"); if (t.Length() == 1) Trash = Root->FindSub(t[0]); } if (Trash) { LArray MoveItems; for (unsigned i=0; i(di); if (t && t->Parent != Trash) { // Move the item to the trash instead... MoveItems.Add(di); Items.DeleteAt(i--, true); } else { LMail3Folder *f = dynamic_cast(di); if (f && f->Parent != Trash) { // Move the folder to the trash instead... MoveItems.Add(di); Items.DeleteAt(i--, true); } } } if (MoveItems.Length() > 0) { Store3Status s = Move(Trash, MoveItems); if (s != Store3Success) return s; } if (Items.Length() == 0) return Store3Success; } Store3Status s = Store3Success; LDataI *Item = Items[0]; switch ((uint32_t)Item->Type()) { default: { LMail3Thing *t = dynamic_cast(Item); if (!t) { LAssert(!"What are you trying to do?"); s = Store3Error; } else if (Callback && !Callback->OnDelete(t->Parent, Items)) { s = Store3Error; } else { for (unsigned i=0; i(Items[i]))) { if (t->DbDelete()) { if (t->Parent) t->Parent->Items.Delete(t); else LAssert(0); DeleteObj(t); } else s = Store3Error; } } } break; } case MAGIC_ATTACHMENT: { for (auto i: Items) { LMail3Attachment *a = dynamic_cast(i); if (a) { a->Delete(ToTrash); a->Detach(); delete a; } } break; } case MAGIC_FOLDER: { LMail3Folder *f = dynamic_cast(Item); if (!f) s = Store3Error; else if (Callback && !Callback->OnDelete(f->Parent, Items)) s = Store3Error; else { for (unsigned i=0; i(Items[i]))) { if (f->DbDelete()) { if (f->Parent) f->Parent->Sub.Delete(f); else LAssert(0); DeleteObj(f); } else s = Store3Error; } } } break; } } return s; } Store3Status LMail3Store::Change(LArray &Items, int PropId, LVariant &Value, LOperator Operator) { if (Items.Length() == 0) return Store3Success; if (PropId != FIELD_FLAGS) { LAssert(!"Not impl."); return Store3NotImpl; } int32 Val = Value.CastInt32(); if (!Val) { LAssert(!"One flag must be set"); return Store3Error; } if (Operator != OpPlusEquals && Operator != OpMinusEquals) { LAssert(!"Operator not supported."); return Store3Error; } StoreTrans Tr = StartTransaction(); // Set/unset flag for (unsigned i=0; i Chunk; for (unsigned n=0; n<100 && i(Items[i]); if (!m) { LAssert(!"Invalid object."); continue; } if ( (Operator == OpMinusEquals && (m->Flags & Val)) || (Operator == OpPlusEquals && !(m->Flags & Val)) ) { p.Print("%sId=" LPrintfInt64, Chunk.Length()?" or ":"", m->Id); Chunk.Add(m); } } if (Chunk.Length()) { auto Sql = p.NewLStr(); LStatement s(this, Sql); if (!s.Exec()) return Store3Error; for (unsigned n=0; n(Chunk[n]); if (m) { if (Operator == OpMinusEquals) m->Flags &= ~Val; else m->Flags |= Val; } } OnChange(_FL, Chunk, PropId); } } return Store3Success; } bool LMail3Store::SetFormat(LViewI *Parent, LDataPropI *Props) { if (!Parent || !Props) return false; Mail3SubFormat NewFormat = (Mail3SubFormat)Props->GetInt(Store3UiNewFormat); if (NewFormat == Format) return true; bool Error = false; LStatement Folders(this, "select * from " MAIL3_TBL_FOLDER); char Sql[256]; LStatement Count(this, "select Count(*) as c from " MAIL3_TBL_FOLDER); if (Count.Row()) { char *c = Count.GetStr(0); if (c) Props->SetInt(Store3UiMaxPos, atoi(c)); } if (NewFormat == Mail3v1) { // Change format to single table for all email // Create the mail table Store3Status s = CheckTable(MAIL3_TBL_MAIL, TblMail); if (!UpdateTable(MAIL3_TBL_MAIL, TblMail, s)) Error = true; else { // Copy all the sub-folder mail into that one table... while (Folders.Row()) { int64 Id = Folders.GetInt64(0); char TblName[128]; sprintf_s(TblName, sizeof(TblName), "Mail_" LPrintfInt64, Id); // Set the parent ID on all mail rows in the sub-folder table (just to be sure) sprintf_s(Sql, sizeof(Sql), "update %s set ParentId=" LPrintfInt64, TblName, Id); LStatement SetId(this, Sql); if (!SetId.Exec()) { Error = true; break; } // Now copy all the rows into the main Mail table sprintf_s(Sql, sizeof(Sql), "insert into " MAIL3_TBL_MAIL " select * from %s", TblName); LStatement Copy(this, Sql); if (!Copy.Exec()) { Error = true; break; } // And drop the sub-folder table.. sprintf_s(Sql, sizeof(Sql), "drop table if exists %s", TblName); LStatement Del(this, Sql); if (!Del.Exec()) { Error = true; break; } } } } else if (NewFormat == Mail3v2) { // Change format to a table per folder of email { LTransaction Trans(this); int Pos = 0; while (Folders.Row()) { int64 Id = Folders.GetInt64(0); char TblName[128]; sprintf_s(TblName, sizeof(TblName), "Mail_" LPrintfInt64, Id); sprintf_s(Sql, sizeof(Sql), "create table if not exists %s as select * from Mail where ParentId=" LPrintfInt64, TblName, Id); LStatement Convert(this, Sql); if (!Convert.Exec()) { Error = true; Trans.RollBack(); break; } Props->SetInt(Store3UiCurrentPos, ++Pos); } } sprintf_s(Sql, sizeof(Sql), "drop table if exists " MAIL3_TBL_FOLDER); LStatement Del(this, Sql); if (!Del.Exec()) Error = true; } if (!Error) Format = NewFormat; return !Error; } void LMail3Store::Upgrade(LViewI *Parent, LDataPropI *Props, std::function OnStatus) { bool Status = true; LStringPipe p; for (auto it : Fields) { Store3Status s = TableStatus.Find(it.key); if (s == Store3UpgradeRequired) { if (!UpdateTable(it.key, it.value, s)) { p.Print("Failed to upgrade %s\n", it.key); Status = false; } } } if (Props && !Status) { LAutoString a(p.NewStr()); Props->SetStr(Store3UiError, a); } if (OnStatus) OnStatus(Status); } class SqliteRepairThread : public LThread { LMail3Store *Store; LString Exe; LString Db; LString RepairSql; LString OldDb; LViewI *Parent = NULL; LDataPropI *Props = NULL; LAutoPtr Shell; ssize_t Ch = 0; char Line[512] = {}; public: bool Status = false; SqliteRepairThread(LMail3Store *store, LString exe, LString db, LViewI *parent, LDataPropI *props) : Store(store), LThread("SqliteRepairThread") { Exe = exe; Db = db; Parent = parent; Props = props; char p[MAX_PATH_LEN]; LMakePath(p, sizeof(p), Db, "..\\Repair.sql"); for (char *c = p; *c; c++) { if (*c == '\\') *c = '/'; } RepairSql = p; LString s; LDateTime dt; dt.SetNow(); char sNow[64]; dt.Get(sNow, sizeof(sNow)); for (char *c = sNow; *c; c++) { if (*c == ':' || *c == '/') *c = '-'; } s.Printf("..\\Database %s.sqlite", sNow); LMakePath(p, sizeof(p), Db, s); OldDb = p; LgiTrace("Sqlite Repair paths:\n" "Exe: '%s'\n" "Db: '%s'\n" "Old: '%s'\n" "Sql: '%s'\n", Exe.Get(), Db.Get(), OldDb.Get(), RepairSql.Get()); Run(); } char *Read() { Ch = Shell->Read(Line, sizeof(Line)-1); if (Ch < 0) return NULL; Line[Ch] = 0; return Line; } bool GetPrompt() { char *l; while ((l = Read())) { if (stristr(l, "sqlite>")) return true; } if (Props) Props->SetStr(Store3UiError, "Failed to get prompt..."); return false; } void StatusMsg(const char *msg) { if (Props) { int64 p = Props->GetInt(Store3UiCurrentPos); Props->SetInt(Store3UiCurrentPos, p + 1); Props->SetStr(Store3UiStatus, msg); } } int OnStatus(int s) { // Send status event back to the store... auto msg = new LMail3StoreMsg(LMail3StoreMsg::MsgRepairComplete); msg->Int = s; Store->PostStore(msg); return s; } int Main() { if (Props) { Props->SetInt(Store3UiMaxPos, 7); Props->SetStr(Store3UiStatus, "Starting sqlite..."); } // Open the shell and start the export process LString Args; Args.Printf("-interactive %s", Db.Get()); if (!Shell.Reset(new LSubProcess(Exe, Args))) return OnStatus(-1); if (!Shell->Start(true, true)) { if (Props) Props->SetStr(Store3UiError, "Couldn't execute sqlite3 shell."); return OnStatus(-2); } // http://froebe.net/blog/2015/05/27/error-sqlite-database-is-malformed-solved/ // // Run through the commands: // pragma integrity_check; // .mode insert // .output mydb_export.sql // .dump // .exit if (!GetPrompt()) return OnStatus(-3); StatusMsg("Setting mode..."); LString Cmd; Cmd.Printf(".mode insert\r\n"); Shell->Write(Cmd, Cmd.Length()); if (!GetPrompt()) return OnStatus(-4); StatusMsg("Setting output file..."); Cmd.Printf(".output \"%s\"\r\n", RepairSql.Get()); Shell->Write(Cmd, Cmd.Length()); if (!GetPrompt()) return OnStatus(-5); StatusMsg("Dumping SQL..."); Cmd.Printf(".dump\r\n"); Shell->Write(Cmd, Cmd.Length()); if (!GetPrompt()) return OnStatus(-6); Cmd.Printf(".exit\r\n"); Shell->Write(Cmd, Cmd.Length()); Shell->Wait(); StatusMsg("Renaming database..."); if (!FileDev->Move(Db, OldDb)) { LString s; s.Printf("Can't move '%s' to '%s'", Db.Get(), OldDb.Get()); Props->SetStr(Store3UiError, s); return -6; } StatusMsg("Importing SQL..."); char *r = RepairSql; for (char *c = r; *c; c++) { if (*c == '/' || *c == '\\') *c = DIR_CHAR; } #ifdef WINDOWS wchar_t wd[MAX_PATH_LEN]; _wgetcwd(wd, MAX_PATH_LEN); #else char wd[MAX_PATH_LEN]; #ifdef __GTK_H__ // Gtk:: #endif getcwd(wd, MAX_PATH_LEN); #endif char base[MAX_PATH_LEN]; LMakePath(base, sizeof(base), Db, ".."); #ifdef WINDOWS LAutoWString baseW(Utf8ToWide(base)); _wchdir(baseW); #else #ifdef __GTK_H__ // Gtk:: #endif chdir(base); #endif char *exe_leaf = strrchr(Exe, DIR_CHAR); Args.Printf("%s \"%s\" < \"%s\"", exe_leaf ? exe_leaf + 1 : Exe.Get(), Db.Get(), RepairSql.Get()); int Result = system(Args); LgiTrace("Import result = %i\n", Result); #ifdef WINDOWS _wchdir(wd); #else #ifdef __GTK_H__ // Gtk:: #endif chdir(wd); #endif StatusMsg("Deleting temporary files..."); FileDev->Delete(RepairSql, false); return OnStatus(0); } }; bool CheckPathForFile(const char *File, char *Exe, int ExeSize) { LString Path; #ifdef WINDOWS char *buffer = NULL; size_t sz = 0; errno_t err = _dupenv_s(&buffer, &sz, "PATH"); if (err) { LgiTrace("%s:%i - _dupenv_s failed with %i.\n", _FL, err); return false; } if (sz == 0) // PATH not found return false; Path = buffer; free(buffer); #else Path = getenv("PATH"); if (!Path) return false; #endif LString::Array p = Path.Split(LGI_PATH_SEPARATOR); for (unsigned i=0; i OnStatus) { // Is the sqlite3 shell binary available? char base[MAX_PATH_LEN]; char exe[MAX_PATH_LEN] = ""; LMakePath(base, sizeof(base), DbFile, ".."); #ifdef WINDOWS const char *SqliteBin = "sqlite3.exe"; LMakePath(exe, sizeof(exe), base, SqliteBin); #else const char *SqliteBin = "sqlite3"; CheckPathForFile(SqliteBin, exe, sizeof(exe)); #endif if (!LFileExists(exe)) { LString DownloadUrl = "https://www.sqlite.org/download.html"; LViewI *p = dynamic_cast(Props); LString Msg; Msg.Printf("Error: sqlite shell binary missing (%s). " #ifdef WINDOWS "Download from:\n" "\n" " %s\n" #else "Install using your package manager:\n" "\n" " e.g. sudo apt-get install sqlite3\n" #endif "\n" "Download the shell binary and extract to this folder:\n" "\n" " %s\n" "\n" "Then try the Repair command again.\n", SqliteBin, DownloadUrl.Get(), base); auto Dlg = new LAlert(p?p:Parent, "LMail3Store::Repair", Msg, "Browse Download Site & Local Folder", "Cancel"); Dlg->DoModal([this, Dlg, DownloadUrl, base](auto dlg, auto ctrlId) { if (ctrlId == 1) { LExecute(DownloadUrl); LExecute(base); } delete dlg; }); if (OnStatus) OnStatus(true); return; } // Close our database... CloseDb(); // Do the repair in a thread RepairOnStatus = OnStatus; new SqliteRepairThread(this, exe, DbFile, Parent, Props); } int64 LMail3Store::GetFolderId(char *Path) { auto Parts = LString(Path).SplitDelimit("/"); LMail3Folder *Root = dynamic_cast(GetRoot()); if (!Root) return 0; uint64 Id = Root->Id; for (unsigned i=0; iSetStr(Store3UiError, s); } void SetStatus(const char *s) { Props->SetStr(Store3UiStatus, s); } void OnStatus(bool s) { // Send status event back to the store... auto msg = new LMail3StoreMsg(LMail3StoreMsg::MsgCompactComplete); msg->Int = s; Store->PostStore(msg); } int Main() { // Clean up orphaned mail segments. { SetStatus("Cleaning up orphaned Mail segments..."); LMail3Store::LStatement s(Store, "delete from " MAIL3_TBL_MAILSEGS " where MailId not in (select Id from " MAIL3_TBL_MAIL ")"); if (!s.Exec()) { SetError("Failed to delete orphaned segments."); OnStatus(false); return -1; } } // Clean up orphaned mail. if (!IsCancelled()) { SetStatus("Cleaning up orphaned Mail..."); LMail3Store::LStatement count(Store, "select COUNT(*) from " MAIL3_TBL_MAIL " where ParentId not in (select Id from " MAIL3_TBL_FOLDER ")"); if (count.Row()) { int64 Rows = count.GetInt64(0); Max = Rows; } LDataStoreI::StoreTrans Trn = Store->StartTransaction(); LMail3Store::LStatement s(Store, "select Id from " MAIL3_TBL_MAIL " where ParentId not in (select Id from " MAIL3_TBL_FOLDER ")"); int64 RowPos = 0; while (!IsCancelled() && s.Row()) { int64 Id = s.GetInt64(0); Store->DeleteMailById(Id); Value = ++RowPos; } Max = 1; Value = 0; } // Vacuum if (!IsCancelled()) { SetStatus("Vacuum unused space..."); LMail3Store::LStatement s(Store, "vacuum;"); if (!s.Exec()) { SetError("Reclaiming space failed."); OnStatus(false); return -1; } } OnStatus(true); return 0; } }; void LMail3Store::Compact(LViewI *Parent, LDataPropI *Props, std::function OnStatus) { if (!Props || !Callback) return; bool Status = false; LVariant InboxPath; if (!Callback->GetSystemPath(FOLDER_INBOX, InboxPath)) Props->SetStr(Store3UiError, "Couldn't get path to Inbox."); else { int64 InboxId = GetFolderId(InboxPath.Str()); if (!InboxId) Props->SetStr(Store3UiError, "Couldn't get Inbox ID."); { // Save this locally, because the thread isn't suitable for calling it. // Instead it will send us a message, and then the store can call it from // the GUI thread. This way any client implementing the callback can talk // to or delete UI elements. CompactOnStatus = OnStatus; new CompactThread(this, InboxId, Props); return; // without calling OnStatus, the CompactThread will do it. } } OnStatus(Status); } void LMail3Store::OnEvent(void *Param) { auto msg = (LMail3StoreMsg*)Param; switch (msg->Msg) { case LMail3StoreMsg::MsgCompactComplete: { if (CompactOnStatus) { CompactOnStatus(msg->Int); CompactOnStatus = NULL; } else LgiTrace("%s:%i - No CompactOnStatus to call on MsgCompactComplete.\n", _FL); break; } case LMail3StoreMsg::MsgRepairComplete: { OpenDb(); if (RepairOnStatus) { RepairOnStatus(msg->Int >= 0); RepairOnStatus = NULL; } break; } default: { LgiTrace("%s:%i - Unhandled mail3store event.\n", _FL); break; } } } bool LMail3Store::IsOk() { return #ifndef __llvm__ this != 0 && #endif Db != 0; } bool LMail3Store::ParseTableFormat(const char *Name, TableDefn &Defs) { char Sql[256]; sprintf_s(Sql, sizeof(Sql), "SELECT * FROM sqlite_master WHERE type='table' and name='%s'", Name); LStatement st(this, Sql); if (!st.IsOk()) return false; if (!st.Row()) return false; char *Fmt = st.GetStr(4); char *s = Fmt ? strchr(Fmt, '(') + 1 : 0; char *e = Fmt ? strrchr(Fmt, ')') : 0; if (!s || !e) return false; LString f(s, e - s); Defs.t = f.SplitDelimit(","); for (unsigned i=0; i= Defs.Length()) { LString Msg; Msg.Printf("\nTable '%s' is missing the schema field: '%s %s'", Name, Flds[i].Type, Flds[i].Name); StatusMsg += Msg; break; } GMail3Def &Def = Defs[i]; if (i >= FieldCount) // Schema field count is less than existing table { LString Msg; Msg.Printf("\nTable '%s' has an extra field: '%s %s'\n", Name, Def.Type, Def.Name); StatusMsg += Msg; break; } if (!Flds[i].Name || !Flds[i].Type || !Def.Name || !Def.Type) { Status = Store3Error; break; } if (_stricmp(Flds[i].Name, Def.Name) != 0 || _stricmp(Flds[i].Type, Def.Type) != 0) { LString Msg; Msg.Printf("\nTable '%s' has a field '%s %s' which is to the schema: '%s %s'\n", Name, Def.Type, Def.Name, Flds[i].Type, Flds[i].Name); StatusMsg += Msg; Status = Store3UpgradeRequired; break; } } if (Defs.Length() != FieldCount) { LString Msg; Msg.Printf("\nTable '%s' has %i fields (should have %i)\n", Name, Defs.Length(), FieldCount); StatusMsg += Msg; Status = Store3UpgradeRequired; } } else Status = Store3Missing; return Status; } bool LMail3Store::UpdateTable(const char *Name, GMail3Def *Flds, Store3Status Check) { LStatement st(this); LAutoString TempTable; TableDefn Defs; if (Check == Store3UpgradeRequired) { // Get the old table format so we can copy over field by field later... if (!ParseTableFormat(Name, Defs)) { return false; } char Tmp[256]; sprintf_s(Tmp, sizeof(Tmp), "%s_tmp", Name); TempTable.Reset(NewStr(Tmp)); char AlterSql[256]; sprintf_s(AlterSql, sizeof(AlterSql), "alter table %s rename to %s", Name, Tmp); LStatement Alter(this, AlterSql); if (!Alter.Exec()) { LAssert(!"Can't rename table."); return false; } } // Create table? LStringPipe p; LHashTbl, bool> AllFlds; p.Print("create table %s (", Name); for (int i=0; Flds[i].Name; i++) { AllFlds.Add(Flds[i].Name, true); if (i) p.Print(", "); p.Print("%s %s", Flds[i].Name, Flds[i].Type); } p.Print(")"); LAutoString s(p.NewStr()); if (s) { if (st.Prepare(s)) st.Exec(); else { LgiTrace("Sql='%s'\n", s.Get()); return false; } } // Copy over data from the old table if (TempTable) { LStringPipe p; p.Print("insert into %s (", Name); int Count = 0; for (unsigned i=0; iGetFields(Tbl); if (f) { LVariant v; LStringPipe p; p.Print("insert into '%s' values (", Tbl); for (int i=0; f[i].Name; i++) { if (i) p.Print(","); p.Print("?"); } p.Print(")"); v.OwnStr(p.NewStr()); Prepare(v.Str()); } } LMail3Store::LUpdate::LUpdate(LMail3Store *store, const char *Tbl, int64 rowid, char *ExcludeField) : LStatement(store) { RowId = rowid; Store = store; Table = Tbl; LAssert(Store != 0 && Tbl != 0 && RowId > 0); GMail3Def *f = Store->GetFields(Tbl); if (f) { LVariant v; LStringPipe p; p.Print("update %s set ", Tbl); for (int i=1; f[i].Name; i++) { if (ExcludeField && !_stricmp(f[i].Name, ExcludeField)) continue; if (i>1) p.Print(", "); p.Print("%s=?%i", f[i].Name, i + 1); } p.Print(" where %s=?1", f[0].Name); v.OwnStr(p.NewStr()); Prepare(v.Str()); } } //////////////////////////////////////////////////////////////////////// bool LMail3Obj::Check(int r, char *sql) { return Store->Check(r, sql); } #define DEBUG_MAIL3_WRITE 0 bool LMail3Obj::Write(const char *Table, bool Insert) { #if DEBUG_MAIL3_WRITE LProfile Prof("LMail3Obj::Write"); #endif LAutoPtr s; #if DEBUG_MAIL3_WRITE Prof.Add(_FL); #endif if (Insert) s.Reset(new LMail3Store::LInsert(Store, Table)); else s.Reset(new LMail3Store::LUpdate(Store, Table, Id)); #if DEBUG_MAIL3_WRITE Prof.Add(_FL); #endif if (s) { Serialize(*s, true); #if DEBUG_MAIL3_WRITE Prof.Add(_FL); #endif if (s->Exec()) { #if DEBUG_MAIL3_WRITE Prof.Add(_FL); #endif if (Insert) { Id = s->LastInsertId(); if (Id < 0) { LAssert(!"No ID returned."); LgiTrace("%s:%i - No ID from statement.\n", _FL); } } } else { LgiTrace("%s:%i - Exec failed.\n", _FL); } } else LAssert(!"No statement"); return Id >= 0; } ////////////////////////////////////////////////////////////////////////// LMail3Store::LStatement::LStatement(LMail3Store *store, const char *sql) { Store = store; s = 0; #if MAIL3_TRACK_OBJS LMail3Store::SqliteObjs &_d = Store->All.New(); _d.Stat = this; #endif if (sql) Prepare(sql); } LMail3Store::LStatement::~LStatement() { Finalize(); #if MAIL3_TRACK_OBJS Store->RemoveFromAll(this); #endif } int64 LMail3Store::LStatement::LastInsertId() { return sqlite3_last_insert_rowid(Store->GetDb()); } bool LMail3Store::LStatement::Prepare(const char *Sql) { Finalize(); TempSql.Reset(NewStr(Sql)); Store->Check(sqlite3_prepare_v2(Store->GetDb(), Sql, -1, &s, 0), Sql); return s != 0; } bool LMail3Store::LStatement::Finalize() { if (!s) return false; bool Status = Store->Check(sqlite3_finalize(s), 0); s = 0; return Status; } bool LMail3Store::LStatement::Row() { if (!IsOk()) return false; int r = sqlite3_step(s); return r == SQLITE_ROW; } bool LMail3Store::LStatement::Exec() { if (!IsOk()) return false; if (!Store->Check(sqlite3_step(s), TempSql)) return false; bool Status = true; if (Post.Length()) { int64 Id = GetRowId(); LAssert(Id > 0); for (unsigned i=0; iCheck(sqlite3_blob_open( Store->GetDb(), 0, Table.Str(), b.ColName.Str(), Id, true, &Blob), 0)) { LArray Buf; if (Buf.Length(32<<10)) { ssize_t r, Pos = 0; b.Data->SetPos(0); while ((r = b.Data->Read(&Buf[0], Buf.Length())) > 0) { if (!Store->Check(sqlite3_blob_write(Blob, &Buf[0], (int)r, (int)Pos), 0)) { Status = false; break; } Pos += r; } } else Status = false; Store->Check(sqlite3_blob_close(Blob), 0); } } Post.Length(0); } return Status; } bool LMail3Store::LStatement::Reset() { if (!IsOk()) return false; return Store->Check(sqlite3_reset(s), 0) && Store->Check(sqlite3_clear_bindings(s), 0); } /* All the getter functions use 'Col' as the first column is '0' */ int LMail3Store::LStatement::GetSize(int Col) { return IsOk() ? sqlite3_column_bytes(s, Col) : 0; } bool LMail3Store::LStatement::GetBool(int Col) { if (!IsOk() || sqlite3_column_type(s, Col) == SQLITE_NULL) return false; return sqlite3_column_int(s, Col) != 0; } int LMail3Store::LStatement::GetInt(int Col) { if (!IsOk() || sqlite3_column_type(s, Col) == SQLITE_NULL) return -1; return sqlite3_column_int(s, Col); } int64 LMail3Store::LStatement::GetInt64(int Col) { if (!IsOk() || sqlite3_column_type(s, Col) == SQLITE_NULL) return -1; return sqlite3_column_int64(s, Col); } bool LMail3Store::LStatement::SetInt64(int Col, int64 n) { if (!IsOk()) return false; if (n == -1) return Store->Check(sqlite3_bind_null(s, Col+1), 0); else return Store->Check(sqlite3_bind_int64(s, Col+1, n), 0); } char *LMail3Store::LStatement::GetStr(int Col) { if (!IsOk()) return NULL; auto txt = (char*)sqlite3_column_text(s, Col); return txt; } bool LMail3Store::LStatement::GetBinary(int Col, LVariant *v) { if (!v) return false; const void *Ptr = sqlite3_column_blob(s, Col); if (!Ptr) return false; int Bytes = sqlite3_column_bytes(s, Col); return v->SetBinary(Bytes, (void*)Ptr); } /* All the setter functions use 'Col+1' as the first column is '1' */ bool LMail3Store::LStatement::SetInt(int Col, int n) { if (!IsOk()) return false; if (n == -1) return Store->Check(sqlite3_bind_null(s, Col+1), 0); else return Store->Check(sqlite3_bind_int(s, Col+1, n), 0); } -bool LMail3Store::LStatement::SetStr(int Col, char *Str) +bool LMail3Store::LStatement::SetStr(int Col, const char *Str) { - if (!IsOk()) return false; + if (!IsOk()) + return false; + if (Str) return Store->Check(sqlite3_bind_text(s, Col+1, Str, -1, SQLITE_STATIC), 0); else return Store->Check(sqlite3_bind_null(s, Col+1), 0); } bool LMail3Store::LStatement::SetDate(int Col, LDateTime &Var) { if (!Var.Year()) return Store->Check(sqlite3_bind_null(s, Col+1), 0); char c[64]; sprintf_s(c, sizeof(c), "%4.4i-%2.2i-%2.2i %2.2i:%2.2i:%2.2i", Var.Year(), Var.Month(), Var.Day(), Var.Hours(), Var.Minutes(), Var.Seconds()); return Store->Check(sqlite3_bind_text(s, Col+1, (const char*)c, -1, SQLITE_TRANSIENT), 0); }; bool LMail3Store::LStatement::SetStream(int Col, const char *ColName, LStreamI *Data) { if (!IsOk() || !Data) return false; PostBlob &p = Post.New(); p.ColName = ColName; p.Data = Data; p.Size = Data->GetSize(); LAssert((p.Size & 0xffffffff00000000) == 0); return Store->Check(sqlite3_bind_zeroblob(s, Col+1, (int)p.Size), 0); } bool LMail3Store::LStatement::SetBinary(int Col, const char *ColName, LVariant *v) { if (!v || v->Type != GV_BINARY) { LAssert(!"Invalid binary type."); return false; } if (!Store->Check(sqlite3_bind_blob(s, Col+1, v->Value.Binary.Data, (int)v->Value.Binary.Length, SQLITE_TRANSIENT), NULL)) return false; return true; } ////////////////////////////////////////////////////////////////////////// LMail3Store::LTransaction::LTransaction(LMail3Store *store) { Store = store; const char *Sql = "begin;"; Open = Store ? Store->Check(sqlite3_exec(Store->GetDb(), Sql, 0, 0, 0), Sql) : false; } LMail3Store::LTransaction::~LTransaction() { if (Open) { const char *Sql = "end;"; Store->Check(sqlite3_exec(Store->GetDb(), Sql, 0, 0, 0), Sql); } } bool LMail3Store::LTransaction::RollBack() { bool Status = false; if (Open) { const char *Sql = "rollback;"; Status = Store->Check(sqlite3_exec(Store->GetDb(), Sql, 0, 0, 0), Sql); Open = false; } return Status; } ///////////////////////////////////////////////////////////////////////// LDataStoreI *OpenMail3(const char *Mail3Folder, LDataEventsI *Callback, bool Create) { return new LMail3Store(Mail3Folder, Callback, Create); }