diff --git a/src/ImpExp_Mbox.cpp b/src/ImpExp_Mbox.cpp --- a/src/ImpExp_Mbox.cpp +++ b/src/ImpExp_Mbox.cpp @@ -1,350 +1,349 @@ #include #include #include #include #include "Scribe.h" -#include "lgi/common/NetTools.h" #include "lgi/common/Edit.h" #include "lgi/common/TextLabel.h" #include "lgi/common/TextFile.h" #include "lgi/common/LgiRes.h" #include "lgi/common/FileSelect.h" #include "FolderTask.h" #include "resdefs.h" /////////////////////////////////////////////////////////////////////////// ImportExportDlg::ImportExportDlg ( ScribeWnd *parent, bool IsExport, const char *Title, const char *Msg, LString::Array *srcFiles, const char *dstFolder, int FolderType ) { Type = FolderType; SetParent(App = parent); Export = IsExport; if (LoadFromResource(Export ? IDD_FILES_EXPORT : IDD_FILES_IMPORT)) { Name(Title); GetViewById(IDC_SRC, Src); GetViewById(IDC_DST, Dst); if (Src) { Src->ShowColumnHeader(false); if (srcFiles) { for (auto f: *srcFiles) InsertFile(f); } } else LAssert(0); if (Dst) { if (dstFolder) Dst->Name(dstFolder); } else LAssert(0); SetCtrlName(IDC_MSG, Msg); } MoveToCenter(); } void ImportExportDlg::InsertFile(const char *f) { if (!Src) return; bool Has = false; for (auto n : *Src) { auto path = n->GetText(0); if (Stricmp(path, f) == 0) { Has = true; break; } } if (Has) return; LListItem *n = new LListItem(f); if (!n) return; Src->Insert(n); } int ImportExportDlg::OnNotify(LViewI *Ctrl, LNotification n) { if (!Src || !Dst) return 0; switch (Ctrl->GetId()) { case IDC_ADD: { if (Export) { // Pick scribe folders to export: auto Dlg = new FolderDlg(this, App, Type); Dlg->DoModal([this, Dlg](auto dlg, auto ctrlId) { if (ctrlId) { auto f = Dlg->Get(); if (f) InsertFile(f); } delete dlg; }); } else { // Pick external MBOX files to import: auto s = new LFileSelect(this); s->MultiSelect(true); s->Type("All Files", LGI_ALL_FILES); s->Type("MBOX Files", "*.mbx;*.mbox"); s->Type("Outlook Express Folders", "*.mbx;*.dbx"); s->Type("Mozilla Address Book", "*.mab"); s->Type("Eudora Address Book", "NNdbase.txt"); s->Open([this](auto dlg, auto status) { if (status) { for (int i=0; iLength(); i++) InsertFile((*dlg)[i]); } delete dlg; }); } break; } case IDC_SRC: { if (n.Type == LNotifyDeleteKey) { List Sel; if (Src->GetSelection(Sel)) Sel.DeleteObjects(); } break; } case IDC_DEL: { List Sel; if (Src->GetSelection(Sel)) Sel.DeleteObjects(); break; } case IDC_SET_DEST: { if (Export) { // Pick external folder for writing to: auto s = new LFileSelect(this); s->MultiSelect(!Export); s->OpenFolder( [this](auto s, auto status) { if (status) SetCtrlName(IDC_DST, s->Name()); delete s; }); } else { // Pick scribe folder for writing to: auto Dlg = new FolderDlg(this, App, Type); Dlg->DoModal([this, Dlg](auto dlg, auto ctrlId) { if (ctrlId) { auto f = Dlg->Get(); if (f && Dst) Dst->Name(f); } delete dlg; }); } break; } case IDOK: { for (auto n : *Src) SrcFiles.Add(n->GetText()); DestFolder = Dst->Name(); IncSubFolders = GetCtrlValue(IDC_SUB_FOLDERS) > 0; EndModal(1); break; } case IDCANCEL: { EndModal(0); break; } } return 0; } /////////////////////////////////////////////////////////////////////////// void Import_UnixMBox(ScribeWnd *Parent) { auto Cur = Parent->GetCurrentFolder(); LString DstPath; if (Cur) DstPath = Cur->GetPath(); auto Dlg = new ImportExportDlg( Parent, false, LLoadString(IDS_MBOX_IMPORT), LLoadString(IDS_MBOX_SELECT_FOLDER), NULL, // Src DstPath); Dlg->DoModal([Dlg, Parent](auto dlg, auto ok) { if (ok && Dlg->DestFolder) { auto Folder = Parent->GetFolder(Dlg->DestFolder); if (Folder) { for (auto File: Dlg->SrcFiles) { LAutoPtr F(new LTextFile); if (F->Open(File, O_READ)) Folder->Import(Folder->AutoCast(F), sMimeMbox); } } } delete dlg; }); } struct MboxExportTask : public LView::ViewEventTarget { ScribeWnd *App = NULL; LString DestFolder; LString::Array SrcFiles; bool IncSubFolders = false; // Chain together enough ExportFolderTask instances to // get the SrcFiles written.... MboxExportTask(ScribeWnd *Parent) : LView::ViewEventTarget(Parent, M_EXPORT_NEXT) { // Kick it off.... App = Parent; } void Next() { PostEvent(M_EXPORT_NEXT); } LMessage::Result OnEvent(LMessage *Msg) { if (Msg->Msg() == M_EXPORT_NEXT) { if (SrcFiles.Length() == 0) return OnFinished(); // Pop next folder path auto inPath = SrcFiles[0]; SrcFiles.DeleteAt(0, true); auto folder = App->GetFolder(inPath); if (folder) { LFile::Path outPath = DestFolder; outPath += LGetLeaf( folder->GetDropFileName() ); auto p = outPath.GetFull(); LAutoPtr outFile; outFile.Reset(new LFile(p, O_WRITE)); new ExportFolderTask(folder, outFile, sMimeMbox, [this](auto prog, auto stream) { Next(); }); return 1; } Next(); } return 0; } int OnFinished() { delete this; return 0; } }; void Export_UnixMBox(ScribeWnd *Parent) { ScribeFolder *Cur = Parent->GetCurrentFolder(); LString::Array SrcPath; if (Cur) SrcPath.Add(Cur->GetPath()); auto Dlg = new ImportExportDlg( Parent, true, LLoadString(IDS_MBOX_EXPORT), LLoadString(IDS_MBOX_EXPORT_FOLDER), &SrcPath); Dlg->DoModal([Dlg, Parent](auto dlg, auto ok) { if (ok && Dlg->SrcFiles.Length() > 0 && Dlg->DestFolder) { auto task = new MboxExportTask(Parent); task->DestFolder = Dlg->DestFolder; task->SrcFiles = Dlg->SrcFiles; task->IncSubFolders = Dlg->IncSubFolders; task->Next(); } delete dlg; }); } /* ScribeFolder *Folder = Parent->GetFolder(Dlg->DestFolder); if (Folder) { for (auto File: Dlg->SrcFiles) { if (!LFileExists(File) || LgiMsg( Parent, LLoadString(IDS_ERROR_FILE_EXISTS), AppName, MB_YESNO, File.Get()) == IDYES) { LAutoPtr F(new LFile); if (F->Open(File, O_WRITE)) Folder->Export(Folder->AutoCast(F), sMimeMbox); } } } */ \ No newline at end of file diff --git a/src/OutlookDropSupport.cpp b/src/OutlookDropSupport.cpp --- a/src/OutlookDropSupport.cpp +++ b/src/OutlookDropSupport.cpp @@ -1,488 +1,487 @@ #ifdef WIN32 #include #include #include #include #include #include "lgi/common/Com.h" -#include "lgi/common/NetTools.h" #include "lgi/common/RtfHtml.h" #include "lgi/common/Variant.h" #ifndef PR_BODY_HTML #define PR_BODY_HTML PROP_TAG(PT_TSTRING, 0x1013) #endif class IDataStream : public LUnknownImpl { ssize_t Len, Pos; char *Data; public: IDataStream(LVariant &v) { Len = v.Value.Binary.Length; Data = new char[Len]; if (Data) { memcpy(Data, v.Value.Binary.Data, Len); } Pos = 0; AddInterface(IID_IStream, this); AddInterface(IID_ISequentialStream, this); } HRESULT STDMETHODCALLTYPE Read(void *pv, ULONG cb, ULONG *pcbRead) { if (pv) { int r = min((unsigned)(Len-Pos), cb); if (r) { memcpy(pv, Data + Pos, r); Pos += r; if (pcbRead) { *pcbRead = r; } return S_OK; } } return S_FALSE; } HRESULT STDMETHODCALLTYPE Write(void const *pv, ULONG cb, ULONG *pcbWritten) { return S_FALSE; } HRESULT STDMETHODCALLTYPE Seek(LARGE_INTEGER dlibMove, DWORD dwOrigin, ULARGE_INTEGER *plibNewPosition) { return S_FALSE; } HRESULT STDMETHODCALLTYPE SetSize(ULARGE_INTEGER libNewSize) { return S_FALSE; } HRESULT STDMETHODCALLTYPE CopyTo(IStream *pstm, ULARGE_INTEGER cb, ULARGE_INTEGER *pcbRead, ULARGE_INTEGER *pcbWritten) { return S_FALSE; } HRESULT STDMETHODCALLTYPE Commit(DWORD grfCommitFlags) { return S_FALSE; } HRESULT STDMETHODCALLTYPE Revert(void) { return S_FALSE; } HRESULT STDMETHODCALLTYPE LockRegion(ULARGE_INTEGER libOffset, ULARGE_INTEGER cb, DWORD dwLockType) { return S_FALSE; } HRESULT STDMETHODCALLTYPE UnlockRegion(ULARGE_INTEGER libOffset, ULARGE_INTEGER cb, DWORD dwLockType) { return S_FALSE; } HRESULT STDMETHODCALLTYPE Stat(STATSTG *pstatstg, DWORD grfStatFlag) { return S_FALSE; } HRESULT STDMETHODCALLTYPE Clone(IStream **ppstm) { return S_FALSE; } }; class OutlookConverter { static bool MapiLoaded; protected: ScribeWnd *App; public: OutlookConverter(ScribeWnd *app) { App = app; } virtual bool GetProp(LVariant &v, int Prop) { return 0; } virtual OutlookConverter *GetAttachment(int i) { return 0; } virtual char *GetHtml() { return 0; } char *GetSegment(char *ContentType) { if (!ContentType) ContentType = "text/plain"; LVariant Section; if (_stricmp(ContentType, "text/plain") == 0) { GetProp(Section, PR_BODY); } else if (_stricmp(ContentType, "text/html") == 0) { if (!GetProp(Section, PR_BODY_HTML)) { Section = GetHtml(); } } return NewStr(Section.Str()); } int GetType() { LVariant Obj; if (GetProp(Obj, PR_BODY)) { return MAGIC_MAIL; } if (GetProp(Obj, PR_GIVEN_NAME)) { return MAGIC_CONTACT; } return MAGIC_NONE; } Mail *GetMail() { Mail *m = 0; LVariant Headers; if (GetProp(Headers, PR_TRANSPORT_MESSAGE_HEADERS) && Headers.Str()) { m = new Mail(App); if (m) { m->App = App; LAutoStreamI Mem( new LMemStream(Headers.Str(), strlen(Headers.Str())) ); m->OnAfterReceive(Mem); m->SetBody(GetSegment("text/plain")); m->SetHtml(GetSegment("text/html")); for (int i=0; true; i++) { OutlookConverter *Oa = GetAttachment(i); if (Oa) { LVariant v; char *Name = 0; if (Oa->GetProp(v, PR_ATTACH_LONG_FILENAME_A) || Oa->GetProp(v, PR_ATTACH_FILENAME_A)) { Name = v.Str(); if (ValidStr(Name)) { Attachment *a = new Attachment(m->App); if (a) { a->App = m->App; a->SetName(Name); if (Oa->GetProp(v, PR_ATTACH_DATA_BIN)) { if (v.Type == GV_BINARY) { a->Set((char*)v.Value.Binary.Data, v.Value.Binary.Length); } } if (Oa->GetProp(v, PR_ATTACH_MIME_TAG_A)) { // We love MIME, We love MIME, We love MIME... ;) a->SetMimeType(v.Str()); } m->AttachFile(a); } } } DeleteObj(Oa); } else break; } } } return m; } Contact *GetContact() { Contact *c = new Contact(App); if (c) { LVariant v; #define MapProp(s, o) \ if (GetProp(v, o) && v.Str()) \ { c->Set(s, v.Str()); } // Simple props MapProp(OPT_First, PR_GIVEN_NAME); MapProp(OPT_Last, PR_SURNAME); MapProp(OPT_HomeStreet, PR_STREET_ADDRESS); MapProp(OPT_HomeSuburb, PR_LOCALITY); MapProp(OPT_HomePostcode, PR_POSTAL_CODE); MapProp(OPT_HomeState, PR_STATE_OR_PROVINCE); MapProp(OPT_HomeCountry, PR_COUNTRY); MapProp(OPT_WorkPhone, PR_BUSINESS_TELEPHONE_NUMBER); MapProp(OPT_HomePhone, PR_HOME_TELEPHONE_NUMBER); MapProp(OPT_HomeMobile, PR_CELLULAR_TELEPHONE_NUMBER); MapProp(OPT_HomeFax, PR_PRIMARY_FAX_NUMBER); MapProp(OPT_HomeWebPage, PR_PERSONAL_HOME_PAGE); MapProp(OPT_WorkWebPage, PR_BUSINESS_HOME_PAGE); MapProp(OPT_Nick, PR_NICKNAME); MapProp(OPT_Spouse, PR_SPOUSE_NAME); MapProp(OPT_Note, PR_BODY); // Email address... fucking M$ programmers try and // hide the email in various undefined tags... // Just keep trying until we get a hit. uint32_t EmailTags[] = { PR_EMAIL_ADDRESS, PR_CONTACT_EMAIL_ADDRESSES }; bool GotEmail = false; int e; for (e=0; eSet(OPT_Email, v.Str()); GotEmail = true; break; } } } if (!GotEmail) { for (e=0x8000; e<0x8100; e++) { if (GetProp(v, PROP_TAG(PT_STRING8, e))) { char *s = v.Str(); if (s && strchr(s, '@')) { c->Set(OPT_Email, v.Str()); GotEmail = true; break; } } } if (!GotEmail) { /* // If the email address isn't getting through... // Try uncommenting this and see what values of i // it stops on for (int i=0x3000; i<= 0x9000; i++) { if (GetProp(v, PROP_TAG(PT_STRING8, i)) && v.Str()) { if (strchr(v.Str(), '@')) { _asm int 3 } } } */ } } } return c; } }; class OutlookIStorage : public OutlookConverter { static bool MapiLoaded; IStorage *Store; bool Release; public: OutlookIStorage(ScribeWnd *App, IStorage *s, bool r) : OutlookConverter(App) { Store = s; Release = r; } ~OutlookIStorage() { if (Release) { Store->Release(); } } char *GetHtml() { char *Ret = 0; LVariant Html; GetProp(Html, PR_RTF_COMPRESSED); LLibrary Mapi("Mapi32"); typedef HRESULT (STDAPICALLTYPE *p_MAPIInitialize)(LPVOID lpMapiInit); typedef void (STDAPICALLTYPE *p_MAPIUninitialize)(); typedef HRESULT(STDAPICALLTYPE *p_WrapCompressedRTFStream)(LPSTREAM lpCompressedRTFStream, ULONG ulFlags, LPSTREAM FAR * lpUncompressedRTFStream); p_MAPIInitialize MAPIInitialize = (p_MAPIInitialize) Mapi.GetAddress("MAPIInitialize"); p_MAPIUninitialize MAPIUninitialize = (p_MAPIUninitialize) Mapi.GetAddress("MAPIUninitialize"); p_WrapCompressedRTFStream WrapCompressedRTFStream = (p_WrapCompressedRTFStream) Mapi.GetAddress("WrapCompressedRTFStream"); if (MAPIInitialize && MAPIUninitialize && WrapCompressedRTFStream) { LDialog *Wait = 0; if (!MapiLoaded) { MapiLoaded = true; Wait = new LDialog; if (Wait) { Wait->Name(AppName); Wait->SetPos(LRect(0, 0, 150, 60)); Wait->MoveToCenter(); Wait->AddView(new LTextLabel(1, 10, 10, -1, -1, "Connecting to MAPI...")); Wait->DoModeless(); Wait->AttachChildren(); } } MAPIInitialize(0); if (Wait) { Wait->EndModeless(); } IDataStream *i = new IDataStream(Html); if (i) { IStream *o; if (WrapCompressedRTFStream(i, 0, &o) == S_OK) { LStringPipe h; char Buf[256]; ULONG r; HRESULT Err; while ((Err = o->Read(Buf, sizeof(Buf), &r)) == S_OK && r > 0) { h.Push(Buf, r); } o->Release(); char *Text = h.NewStr(); if (Text) { LFile f; if (f.Open("d:\\temp\\mail.rtf", O_WRITE)) { f.SetSize(0); f.Write(Text, strlen(Text)); f.Close(); } Ret = MsRtfToHtml(Text); DeleteArray(Text); } } } MAPIUninitialize(); } return Ret; } OutlookConverter *GetAttachment(int i) { OutlookConverter *Status = 0; if (Store) { IStorage *New = 0; char Part[256]; sprintf_s(Part, sizeof(Part), "__attach_version1.0_#%8.8d", i); char16 *Name = Utf8ToWide(Part); HRESULT Err; if ((Err = Store->OpenStorage(Name, 0, STGM_READ | STGM_SHARE_EXCLUSIVE, 0, 0, &New)) == S_OK) { Status = new OutlookIStorage(App, New, true); } DeleteArray(Name); } return Status; } bool GetProp(LVariant &v, int Prop) { bool Status = false; if (Store) { IStream *Str = 0; char Part[256]; STATSTG Stat; ZeroObj(Stat); sprintf_s(Part, sizeof(Part), "__substg1.0_%08.8X", Prop); char16 *Name = Utf8ToWide(Part); HRESULT Err; if ((Err = Store->OpenStream(Name, 0, STGM_READ | STGM_SHARE_EXCLUSIVE, 0, &Str)) == S_OK) { if (Str->Stat(&Stat, STATFLAG_DEFAULT) == S_OK) { int Size = (int)Stat.cbSize.QuadPart; char *Buf = new char[Size + 1]; if (Buf) { ulong Read; if (Str->Read(Buf, Size, &Read) == S_OK) { Buf[Size] = 0; Status = true; switch (PROP_TYPE(Prop)) { case PT_STRING8: { v = Buf; break; } case PT_BINARY: { v.SetBinary(Size, Buf); break; } case PT_UNICODE: { LAssert(0); break; } case PT_I2: { LAssert(0); break; } case PT_I4: { LAssert(0); break; } case PT_I8: { LAssert(0); break; } default: { Status = false; LAssert(0); break; } } } DeleteArray(Buf); } } Str->Release(); } DeleteArray(Name); } return Status; } }; bool OutlookIStorage::MapiLoaded = false; #endif diff --git a/src/Resource.rc b/src/Resource.rc --- a/src/Resource.rc +++ b/src/Resource.rc @@ -1,139 +1,139 @@ // Microsoft Visual C++ generated resource script. // #include "resource.h" #define APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 2 resource. // #include "winres.h" ///////////////////////////////////////////////////////////////////////////// #undef APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // English (U.S.) resources #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) #ifdef _WIN32 LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US #pragma code_page(1252) #endif //_WIN32 ///////////////////////////////////////////////////////////////////////////// // // Icon // // Icon with lowest ID value placed first to ensure application icon // remains consistent on all systems. IDI_APP ICON "..\\Resources\\Icons\\app.ico" IDI_MAIL ICON "..\\Resources\\Icons\\mail.ico" IDI_BLANK ICON "..\\Resources\\Icons\\blank.ico" IDI_SMALL ICON "..\\Resources\\Icons\\small.ico" IDI_CONTACT ICON "..\\Resources\\Icons\\contact.ico" IDI_FILTER ICON "..\\Resources\\Icons\\filter.ico" IDI_CALENDER ICON "..\\Resources\\Icons\\calender.ico" IDI_EVENT ICON "..\\Resources\\Icons\\event.ico" #endif // English (U.S.) resources ///////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////// // English (Australia) resources #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENA) #ifdef _WIN32 LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_AUS #pragma code_page(1252) #endif //_WIN32 ///////////////////////////////////////////////////////////////////////////// // // Icon // // Icon with lowest ID value placed first to ensure application icon // remains consistent on all systems. IDI_ERR ICON "..\\Resources\\Icons\\error.ico" #ifdef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // TEXTINCLUDE // 1 TEXTINCLUDE BEGIN "resource.h\0" END 2 TEXTINCLUDE BEGIN "#include ""afxres.h""\r\n" "\0" END 3 TEXTINCLUDE BEGIN "\r\n" "\0" END #endif // APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // Version // VS_VERSION_INFO VERSIONINFO - FILEVERSION 3,2,171 - PRODUCTVERSION 3,2,171 + FILEVERSION 3,2,180 + PRODUCTVERSION 3,2,180 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS 0x1L #else FILEFLAGS 0x0L #endif FILEOS 0x40004L FILETYPE 0x1L FILESUBTYPE 0x0L BEGIN BLOCK "StringFileInfo" BEGIN BLOCK "0c0904b0" BEGIN VALUE "CompanyName", "Memecode" VALUE "FileDescription", "Scribe" - VALUE "FileVersion", "3,2,171\0" + VALUE "FileVersion", "3,2,180\0" VALUE "InternalName", "Scribe" VALUE "LegalCopyright", "Copyright © 2016" VALUE "OriginalFilename", "Scribe.exe" VALUE "ProductName", "i.Scribe\0" - VALUE "ProductVersion", "3,2,171\0" + VALUE "ProductVersion", "3,2,180\0" END END BLOCK "VarFileInfo" BEGIN VALUE "Translation", 0xc09, 1200 END END #endif // English (Australia) resources ///////////////////////////////////////////////////////////////////////////// #ifndef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 3 resource. // ///////////////////////////////////////////////////////////////////////////// #endif // not APSTUDIO_INVOKED diff --git a/src/ScribeApp.cpp b/src/ScribeApp.cpp --- a/src/ScribeApp.cpp +++ b/src/ScribeApp.cpp @@ -1,13260 +1,13259 @@ /* ** FILE: ScribeApp.cpp ** AUTHOR: Matthew Allen ** DATE: 22/10/1998 ** DESCRIPTION: Scribe email application ** ** Copyright (C) 1998, Matthew Allen ** fret@memecode.com */ // Debug defines // #define PRINT_OUT_STORAGE_TREE // #define TEST_OBJECT_SIZE #define USE_SPELLCHECKER 1 #define USE_INTERNAL_BROWSER 1 // for help #define RUN_STARTUP_SCRIPTS 1 #define PROFILE_ON_PULSE 0 #define TRAY_CONTACT_BASE 1000 #define TRAY_MAIL_BASE 10000 // Includes #include #include #include #include #include #include "Scribe.h" #include "lgi/common/StoreConvert1To2.h" -#include "lgi/common/NetTools.h" #include "lgi/common/ProgressDlg.h" #include "lgi/common/TextLabel.h" #include "lgi/common/Button.h" #include "lgi/common/CheckBox.h" #include "lgi/common/OpenSSLSocket.h" #include "lgi/common/SoftwareUpdate.h" #include "lgi/common/Html.h" #include "lgi/common/TextView3.h" #include "lgi/common/RichTextEdit.h" #include "lgi/common/Browser.h" #include "lgi/common/ClipBoard.h" #include "lgi/common/Store3.h" #include "lgi/common/Growl.h" #include "lgi/common/Edit.h" #include "lgi/common/Box.h" #include "lgi/common/LgiRes.h" #include "lgi/common/SpellCheck.h" #include "lgi/common/SubProcess.h" #include "lgi/common/CssTools.h" #include "lgi/common/Map.h" #include "lgi/common/Charset.h" #include "lgi/common/RefCount.h" #include "ScribePrivate.h" #include "PreviewPanel.h" #include "ScribeStatusPanel.h" #include "ScribeFolderDlg.h" #include "ScribePageSetup.h" #include "Calendar.h" #include "CalendarView.h" #include "ScribeSpellCheck.h" #include "Store3Common.h" #include "PrintContext.h" #include "resource.h" #include "ManageMailStores.h" #include "ReplicateDlg.h" #include "ScribeAccountPreview.h" #include "Encryption/GnuPG.h" #include "Store3Webdav/WebdavStore.h" #include "resdefs.h" #include "../unittests/UnitTest.h" #include "../src/common/Coding/ScriptingPriv.h" #define DEBUG_STORE_EVENTS 0 #if DEBUG_STORE_EVENTS #define LOG_STORE(...) LgiTrace(__VA_ARGS__) #else #define LOG_STORE(...) #endif #define IDM_LOAD_MSG 2000 #define RAISED_LOOK 0 #define SUNKEN_LOOK false #ifdef MAC #define SUNKEN_CTRL false #else #define SUNKEN_CTRL true #endif #if LGI_CARBON #define TRAY_ICON_NONE -1 #define TRAY_ICON_NORMAL -1 #define TRAY_ICON_MAIL 0 #define TRAY_ICON_ERROR 1 #elif defined(WIN32) #define TRAY_ICON_NORMAL 0 #define TRAY_ICON_ERROR 1 #define TRAY_ICON_MAIL 2 #define TRAY_ICON_NONE 3 #else #define TRAY_ICON_NONE -1 #define TRAY_ICON_NORMAL 0 #define TRAY_ICON_ERROR 1 #define TRAY_ICON_MAIL 2 #endif char ScribeThingList[] = "com.memecode.ThingList"; ScribeClipboardFmt *ScribeClipboardFmt::Alloc(bool ForFolders, size_t Size) { ScribeClipboardFmt *obj = (ScribeClipboardFmt*) calloc(sizeof(ScribeClipboardFmt)+((Size-1)*sizeof(Thing*)), 1); if (obj) { memcpy(obj->Magic, ForFolders ? ScribeFolderMagic : ScribeThingMagic, sizeof(obj->Magic)); obj->ProcessId = LAppInst->GetProcessId(); obj->Len = (uint32_t)Size; } return obj; } ScribeClipboardFmt *ScribeClipboardFmt::Alloc(List &Lst) { ScribeClipboardFmt *Fmt = Alloc(false, Lst.Length()); for (unsigned i=0; iThingAt(i, Lst[i]); return Fmt; } ScribeClipboardFmt *ScribeClipboardFmt::Alloc(LArray &Arr) { ScribeClipboardFmt *Fmt = Alloc(false, Arr.Length()); for (unsigned i=0; iThingAt(i, Arr[i]); return Fmt; } bool ScribeClipboardFmt::Is(const char *Type, void *Ptr, size_t Bytes) { // Do we have the minimum bytes for the structure? if (Bytes >= sizeof(ScribeClipboardFmt) && Ptr != NULL) { ScribeClipboardFmt *This = (ScribeClipboardFmt*)Ptr; // Check the magic is the right value if (memcmp(This->Magic, Type, 4) != 0) return false; // Check it's from this process if (This->ProcessId != LAppInst->GetProcessId()) return false; return true; } return false; } Thing *ScribeClipboardFmt::ThingAt(size_t Idx, Thing *Set) { if (memcmp(Magic, ScribeThingMagic, 4)) return NULL; if (Idx >= Len) return NULL; if (Set) Things[Idx] = Set; return Things[Idx]; } ScribeFolder *ScribeClipboardFmt::FolderAt(size_t Idx, ScribeFolder *Set) { if (memcmp(Magic, ScribeFolderMagic, 4)) return NULL; if (Idx >= Len) return NULL; if (Set) Folders[Idx] = Set; return Folders[Idx]; } size_t ScribeClipboardFmt::Sizeof() { return sizeof(*this) + ((Len - 1) * sizeof(Thing*)); } bool OptionSizeInKiB = false; bool ShowRelativeDates = false; const char *MailAddressDelimiters = "\t\r\n;,"; char16 SpellDelim[] = { ' ', '\t', '\r', '\n', ',', ',', '.', ':', ';', '{', '}', '[', ']', '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '_', '-', '+', '=', '|', '\\', '/', '?', '\"', 0 }; const char *DefaultRfXml = "---------- %s ----------\n" "%s: ()\n" "%s: ()\n" "%s: \n" "%s: \n" "\n" "\n" "\n" "\n"; uchar DateTimeFormats[] = { GDTF_DEFAULT, GDTF_DAY_MONTH_YEAR | GDTF_12HOUR, GDTF_MONTH_DAY_YEAR | GDTF_12HOUR, GDTF_YEAR_MONTH_DAY | GDTF_12HOUR, GDTF_DAY_MONTH_YEAR | GDTF_24HOUR, GDTF_MONTH_DAY_YEAR | GDTF_24HOUR, GDTF_YEAR_MONTH_DAY | GDTF_24HOUR }; SystemFolderInfo SystemFolders[] = { {FOLDER_INBOX, OPT_Inbox, NULL}, {FOLDER_OUTBOX, OPT_Outbox, NULL}, {FOLDER_SENT, OPT_Sent, NULL}, {FOLDER_CONTACTS, OPT_Contacts, NULL}, {FOLDER_TRASH, OPT_Trash, NULL}, {FOLDER_CALENDAR, OPT_Calendar, OPT_HasCalendar}, {FOLDER_TEMPLATES, OPT_Templates, OPT_HasTemplates}, {FOLDER_FILTERS, OPT_Filters, OPT_HasFilters}, {FOLDER_GROUPS, OPT_Groups, OPT_HasGroups}, {FOLDER_SPAM, OPT_SpamFolder, OPT_HasSpam}, {-1, 0, 0} }; ScribeBehaviour *ScribeBehaviour::New(ScribeWnd *app) { return 0; } void ScribeOptionsDefaults(LOptionsFile *f) { if (!f) return; f->CreateTag("Accounts"); f->CreateTag("CalendarUI"); f->CreateTag("CalendarUI.Sources"); f->CreateTag("MailUI"); f->CreateTag("ScribeUI"); f->CreateTag("Plugins"); f->CreateTag("Print"); #define DefaultIntOption(opt, def) { LVariant v; if (!f->GetValue(opt, v)) \ f->SetValue(opt, v = (int)def); } #define DefaultStrOption(opt, def) { LVariant v; if (!f->GetValue(opt, v)) \ f->SetValue(opt, v = def); } DefaultIntOption(OPT_DefaultAlternative, 1); DefaultIntOption(OPT_BoldUnread, 1); DefaultIntOption(OPT_PreviewLines, 1); DefaultIntOption(OPT_AutoDeleteExe, 1); DefaultIntOption(OPT_DefaultReplyAllSetting, MAIL_ADDR_BCC); DefaultIntOption(OPT_BlinkNewMail, 1); DefaultIntOption(OPT_MarkReadAfterSeconds, 5); DefaultStrOption(OPT_BayesThreshold, "0.9"); DefaultIntOption(OPT_SoftwareUpdate, 1); DefaultIntOption(OPT_ResizeImgAttachments, false); DefaultIntOption(OPT_ResizeJpegQual, 80); DefaultIntOption(OPT_ResizeMaxPx, 1024); DefaultIntOption(OPT_ResizeMaxKb, 200); DefaultIntOption(OPT_RegisterWindowsClient, 1); DefaultIntOption(OPT_HasTemplates, 0); DefaultIntOption(OPT_HasCalendar, 1); DefaultIntOption(OPT_HasGroups, 1); DefaultIntOption(OPT_HasFilters, 1); DefaultIntOption(OPT_HasSpam, 0); } const char *Store3ItemTypeName(Store3ItemTypes t) { switch (t) { case MAGIC_NONE: return "MAGIC_NONE"; case MAGIC_BASE: return "MAGIC_BASE"; case MAGIC_MAIL: return "MAGIC_MAIL"; case MAGIC_CONTACT: return "MAGIC_CONTACT"; // case MAGIC_FOLDER: return "MAGIC_FOLDER"; case MAGIC_MAILBOX: return "MAGIC_MAILBOX"; case MAGIC_ATTACHMENT: return "MAGIC_ATTACHMENT"; case MAGIC_ANY: return "MAGIC_ANY"; case MAGIC_FILTER: return "MAGIC_FILTER"; case MAGIC_FOLDER: return "MAGIC_FOLDER"; case MAGIC_CONDITION: return "MAGIC_CONDITION"; case MAGIC_ACTION: return "MAGIC_ACTION"; case MAGIC_CALENDAR: return "MAGIC_CALENDAR"; case MAGIC_ATTENDEE: return "MAGIC_ATTENDEE"; case MAGIC_GROUP: return "MAGIC_GROUP"; default: LAssert(0); break; } return "(error)"; } void SetRecipients(ScribeWnd *App, char *Start, LDataIt l, EmailAddressType CC) { while (Start && *Start) { LString Str; char *End = strchr(Start, ','); if (End) { Str.Set(Start, End-Start); Start = End + 1; } else { Str = Start; Start = 0; } if (Str) { ListAddr *a = new ListAddr(App); if (a) { a->CC = CC; if (_strnicmp(Str, "mailto:", 7) == 0) a->sAddr = Str(7,-1); else a->sAddr = Str; l->Insert(a); } } } } static char SoftwareUpdateUri[] = "http://www.memecode.com/update.php"; enum SoftwareStatus { SwError, SwCancel, SwOutOfDate, SwUpToDate }; static LString ExtractVer(const char *s) { char Buf[256], *Out = Buf; for (const char *In = s; *In && Out < Buf + sizeof(Buf) - 1; In++) { if (*In == ' ') break; if (IsDigit(*In) || *In == '.') *Out++ = *In; } *Out++ = 0; return LString(Buf); } void IsSoftwareUpToDate(ScribeWnd *Parent, bool WithUI, bool IncBetas, std::function callback) { // LSoftwareUpdate::UpdateInfo Info // Software update? auto Proxy = Parent->GetHttpProxy(); auto Update = new LSoftwareUpdate(AppName, SoftwareUpdateUri, Proxy); Update->CheckForUpdate( [WithUI, Parent, callback, Update](auto Info, auto errorMsg) { if (Info) { auto LocalVer = LString(ScribeVer).SplitDelimit("."); LString BuildVer = ExtractVer(Info->Build); auto OnlineVer = BuildVer.SplitDelimit("."); if (OnlineVer.Length() != LocalVer.Length()) { LgiTrace("%s:%i - Invalid online version number \"%s\"\n", _FL, Info->Version.Get()); if(callback) callback(SwError, Info); return; } unsigned i; for(i = 0; i < OnlineVer.Length(); i++) { auto l = Atoi(LocalVer[i].Get()); auto o = Atoi(OnlineVer[i].Get()); if(l < o) { if(callback) callback(SwOutOfDate, Info); return; } if(l > o) { if(callback) callback(SwUpToDate, Info); return; } } LDateTime Compile; auto Date = LString(__DATE__).SplitDelimit(" "); Compile.Month(LDateTime::MonthFromName(Date[0])); Compile.Day(atoi(Date[1])); Compile.Year(atoi(Date[2])); Compile.SetTime(__TIME__); bool DateGreaterThenCompile = Info->Date > Compile; if (callback) callback(DateGreaterThenCompile ? SwOutOfDate : SwUpToDate, Info); return; } else if (WithUI) { if (callback) callback(SwCancel, NULL); LgiMsg(Parent, LLoadString(IDS_ERROR_SOFTWARE_UPDATE), AppName, MB_OK, errorMsg); } if (callback) callback(SwError, NULL); }, WithUI ? Parent : NULL, IncBetas); } bool UpgradeSoftware(const LSoftwareUpdate::UpdateInfo *Info, ScribeWnd *Parent, bool WithUI) { bool DownloadUpdate = true; if (WithUI) { char Ds[64]; Info->Date.Get(Ds, sizeof(Ds)); DownloadUpdate = LgiMsg(Parent, LLoadString(IDS_SOFTWARE_UPDATE_DOWNLOAD), AppName, MB_YESNO, Info->Build.Get(), Info->Uri.Get(), Ds) == IDYES; } if (!DownloadUpdate) return false; LAutoString Proxy = Parent->GetHttpProxy(); LSoftwareUpdate Update(AppName, SoftwareUpdateUri, Proxy, ScribeTempPath()); // FIXME: return false; // Update.ApplyUpdate(Info, false, Parent); } void SoftwareUpdate(ScribeWnd *Parent, bool WithUI, bool IncBetas, std::function callback) { // Software update? IsSoftwareUpToDate(Parent, WithUI, IncBetas, [WithUI, Parent, callback](auto s, auto Info) { if (s == SwUpToDate) { if (WithUI) LgiMsg(Parent, LLoadString(IDS_SOFTWARE_CURRENT), AppName, MB_OK); if (callback) callback(false); // we're up to date } else if (s == SwOutOfDate) { auto status = UpgradeSoftware(Info, Parent, WithUI); if (callback) callback(status); // update is going to happen } }); } const char *AppName = "Scribe"; char HelpFile[] = "index.html"; const char OptionsFileName[] = "ScribeOptions"; const char AuthorEmailAddr[] = "fret@memecode.com"; const char AuthorHomepage[] = "http://www.memecode.com"; const char ApplicationHomepage[] = "http://www.memecode.com/scribe.php"; const char CommercialHomepage[] = "http://www.memecode.com/inscribe.php"; const char FaqHomepage[] = "http://www.memecode.com/scribe/faq.php"; const char *DefaultFolderNames[16]; Store3ItemTypes DefaultFolderTypes[] = { MAGIC_MAIL, // Inbox MAGIC_MAIL, // Outbox MAGIC_MAIL, // Sent MAGIC_ANY, // Trash MAGIC_CONTACT, // Contacts MAGIC_MAIL, // Templates MAGIC_FILTER, // Filters MAGIC_CALENDAR, // Calendar Events MAGIC_GROUP, // Groups MAGIC_MAIL, // Spam MAGIC_NONE, MAGIC_NONE, MAGIC_NONE, MAGIC_NONE }; extern void Log(char *File, char *Str, ...); ////////////////////////////////////////////////////////////////////////////// void LogMsg(char *str, ...) { #ifdef _DEBUG char f[256]; LMakePath(f, sizeof(f), LGetExePath(), "log.txt"); if (str) { char buffer[256]; va_list arg; va_start(arg ,str); vsprintf_s(buffer, sizeof(buffer), str, arg); va_end(arg); LFile File; while (!File.Open(f, O_WRITE)) { LSleep(5); } File.Seek(File.GetSize(), SEEK_SET); File.Write(buffer, strlen(buffer)); } else { FileDev->Delete(f, false); } #endif } LString GetFullAppName(bool Platform) { LString Ret = AppName; if (Platform) { LString s; const char *Build = #ifndef _DEBUG "Release"; #else "Debug"; #endif LArray Ver; int Os = LGetOs(&Ver); const char *OsName = LGetOsName(); if (Os == LGI_OS_WIN9X) { switch (Ver[1]) { case 0: OsName = "Win95"; break; case 10: OsName = "Win98"; break; case 90: OsName = "WinME"; break; } } else if (Os == LGI_OS_WIN32 || Os == LGI_OS_WIN64) { if (Ver[0] < 5) { OsName = "WinNT"; } else if (Ver[0] == 5) { if (Ver[1] == 0) OsName = "Win2k"; else OsName = "WinXP"; } else if (Ver[0] == 6) { if (Ver[1] == 0) OsName = "Vista"; else if (Ver[1] == 1) OsName = "Win7"; else if (Ver[1] == 2) OsName = "Win8"; else if (Ver[1] == 3) OsName = "Win8.1"; } else if (Ver[0] == 10) { OsName = "Win10"; } else if (Ver[0] == 11) { // What's the chances eh? OsName = "Win11"; } } s.Printf(" v%s (%s v", ScribeVer, OsName); Ret += s; for (unsigned i=0; iId); Ret += s; } s.Printf(")"); Ret += s; } return Ret; } bool MatchWord(char *Str, char *Word) { bool Status = false; if (Str && Word) { #define IsWord(c) ( IsDigit(c) || IsAlpha(c) ) for (char *s=stristr(Str, Word); s; s=stristr(s+1, Word)) { char *e = s + strlen(Word); if ( (s<=Str || !IsWord(s[-1]) ) && (e[0] == 0 || !IsWord(e[0])) ) { return true; } } } return Status; } ////////////////////////////////////////////////////////////////////////////// ScribePanel::ScribePanel(ScribeWnd *app, const char *name, int size, bool open) : LPanel(name, size, open) { App = app; } bool ScribePanel::Pour(LRegion &r) { if (App) { SetClosedSize(App->GetToolbarHeight()); } return LPanel::Pour(r); } ////////////////////////////////////////////////////////////////////////////// class NoContactType : public Contact { LString NoFace80Path; LString NoFace160Path; public: NoContactType(ScribeWnd *wnd) : Contact(wnd) { } Thing &operator =(Thing &c) override { return *this; } bool GetVariant(const char *Name, LVariant &Value, const char *Array) override { ScribeDomType Fld = StrToDom(Name); int Px = Array ? atoi(Array) : 80; LString &Str = Px == 160 ? NoFace160Path : NoFace80Path; if (!Str) { LString f; f.Printf("NoFace%i.png", Px); Str = LFindFile(f); LAssert(Str != NULL); // This should always resolve. } if (!Str) return false; if (Fld == SdImageHtml) { LString html; html.Printf("\n", Str.Get()); Value = html; return true; } else if (Fld == SdImage) { Value = Str; return true; } return false; } }; class ScribeWndPrivate : public LBrowser::LBrowserEvents, public LVmCallback, public LHtmlStaticInst { LOptionsFile::PortableType InstallMode = LOptionsFile::UnknownMode; public: ScribeWnd *App; uint64 LastTs = 0; int ClipboardFormat = 0; LFont *PreviewFont = NULL; int PrintMaxPages = -1; int NewMailTimeout = -1; bool SendAfterReceive = false; bool IngoreOnClose = false; LAutoString UiTags; LAutoPtr Growl; LArray TrayMenuContacts; bool ExitAfterSend = false; LToolButton *ShowConsoleBtn = NULL; LString MulPassword; LString CalendarSummary; LBox *SubSplit = NULL, *SearchSplit = NULL; LArray ThingSources; int LastLayout = 0; LMenuItem *DisableUserFilters = NULL; LAutoPtr Options; HttpImageThread *ImageLoader = NULL; int LastMinute = -1, LastHour = -1; LArray Store3EventCallbacks; LAutoPtr PrintOptions; LHashTbl, LString> ResFiles; // These are for the LDataEventsI callbacks to store source context // Mainly for debugging where various events came from. const char *CtxFile = NULL; int CtxLine = 0; // Contact no face images LAutoRefPtr NoContact; // Remote content white/blacklists bool RemoteContent_Init = false; LString::Array RemoteWhiteLst, RemoteBlackLst; // Spell checking int AppWndHnd; LAutoPtr SpellerThread; // Missing caps LCapabilityTarget::CapsHash MissingCaps; MissingCapsBar *Bar = NULL; LString ErrSource; // Script file that has an error. Filter *ErrFilter = NULL; // Filter that has scripting error. // Load state bool FoldersLoaded = false; // Bayesian filter LStringPipe BayesLog; // Thread item processing LArray Transfers; // Scripting... LAutoPtr Engine; LArray Scripts; LArray CurrentScripts; LScript *CurrentScript() { return CurrentScripts.Length() ? CurrentScripts.Last() : NULL; } int NextToolMenuId = IDM_TOOL_SCRIPT_BASE; LAutoPtr ScriptToolbar; LArray OnSecondTimerCallbacks; // Encryption LAutoPtr GpgInst; // Unit tests LAutoPtr UnitTestServer; class ScribeTextControlFactory : public LViewFactory { ScribeWnd *Wnd; LView *NewView(const char *Class, LRect *Pos, const char *Text) { if (!_stricmp(Class, "ScribeTextView")) return Wnd->CreateTextControl(-1, 0, true); return NULL; } public: ScribeTextControlFactory(ScribeWnd *wnd) { Wnd = wnd; } } TextControlFactory; ScribeWndPrivate(ScribeWnd *app) : App(app), TextControlFactory(app) { NoContact = new NoContactType(app); NoContact->DecRef(); // 2->1 AppWndHnd = LEventSinkMap::Dispatch.AddSink(App); #ifdef WIN32 ClipboardFormat = RegisterClipboardFormat( #ifdef UNICODE L"Scribe.Item" #else "Scribe.Item" #endif ); #endif LScribeScript::Inst = new LScribeScript(App); if (Engine.Reset(new LScriptEngine(App, LScribeScript::Inst, this))) Engine->SetConsole(LScribeScript::Inst->GetLog()); } ~ScribeWndPrivate() { // Why do we need this? ~LView will take care of it? // LEventSinkMap::Dispatch.RemoveSink(App); Options.Reset(); Scripts.DeleteObjects(); DeleteObj(ImageLoader); Engine.Reset(); DeleteObj(LScribeScript::Inst); } LGrowl *GetGrowl() { if (!Growl && Growl.Reset(new LGrowl)) { LAutoPtr r(new LGrowl::LRegister); r->App = "Scribe"; r->IconUrl = "http://memecode.com/images/scribe/growl-app.png"; LGrowl::LNotifyType &NewMail = r->Types.New(); NewMail.Name = "new-mail"; NewMail.IconUrl = "http://memecode.com/images/scribe/growl-new-mail.png"; NewMail.Enabled = true; LGrowl::LNotifyType &Cal = r->Types.New(); Cal.Name = "calendar"; Cal.IconUrl = "http://memecode.com/images/scribe/growl-calendar.png"; Cal.Enabled = true; LGrowl::LNotifyType &Debug = r->Types.New(); Debug.Name = "debug"; Debug.IconUrl = "http://memecode.com/images/scribe/growl-bug.png"; Debug.Enabled = false; LGrowl::LNotifyType &Info = r->Types.New(); Info.IconUrl = "http://memecode.com/images/scribe/growl-note.png"; Info.Name = "info"; Info.Enabled = true; Growl->Register(r); } return Growl; } LVmDebugger *AttachVm(LVirtualMachine *Vm, LCompiledCode *Code, const char *Assembly) { LVariant v; if (Options) Options->GetValue(OPT_ScriptDebugger, v); if (v.CastInt32()) return new LVmDebuggerWnd(App, this, Vm, Code, NULL); return NULL; } bool CallCallback(LVirtualMachine &Vm, LString CallbackName, LScriptArguments &Args) { for (auto s: Scripts) { if (!s->Code) continue; auto Method = s->Code->GetMethod(CallbackName); if (!Method) continue; auto Status = Vm.ExecuteFunction(s->Code, Method, Args); return Status > ScriptError; } Vm.SetDebuggerEnabled(true); // Lets show the UI when we throw the callback not found error. Args.Throw(_FL, "There is no function '%s' for callback.", CallbackName.Get()); return false; } bool CompileScript(LAutoPtr &Output, const char *FileName, const char *Source) { LCompiler c; return c.Compile(Output, Engine->GetSystemContext(), LScribeScript::Inst, FileName, Source, NULL); } bool OnSearch(LBrowser *br, const char *txt) { char Path[256]; if (!App->GetHelpFilesPath(Path, sizeof(Path))) return false; auto Terms = LString(txt).SplitDelimit(", "); LStringPipe p; p.Print("\n

Search Results

\n
    \n"); LDirectory Dir; for (int b = Dir.First(Path, "*.html"); b; b = Dir.Next()) { if (!Dir.IsDir()) { char Path[256]; Dir.Path(Path, sizeof(Path)); LFile f; if (f.Open(Path, O_READ)) { LXmlTree t(GXT_NO_DOM); LXmlTag r; if (t.Read(&r, &f)) { char *PrevName = 0; char PrevUri[256] = ""; for (auto c: r.Children) { if (c->IsTag("a")) { char *Name = c->GetAttr("name"); if (Name) { PrevName = Name; } } else if (c->GetContent()) { bool Hit = false; for (unsigned i=0; !Hit && iGetContent(), Terms[i]) != 0; } if (Hit) { LStringPipe Uri(256); char *Leaf = strrchr(Path, DIR_CHAR); Leaf = Leaf ? Leaf + 1 : Path; Uri.Print("file://%s", Path); if (PrevName) Uri.Print("#%s", PrevName); LAutoString UriStr(Uri.NewStr()); if (_stricmp(UriStr, PrevUri)) { p.Print("
  • %s", UriStr.Get(), Leaf); if (PrevName) p.Print("#%s", PrevName); p.Print("\n"); strcpy_s(PrevUri, sizeof(PrevUri), UriStr); } } } } } } } } p.Print("
\n\n\n"); LAutoString Html(p.NewStr()); br->SetHtml(Html); return true; } void AskUserForInstallMode(std::function callback) { auto Dlg = new LAlert(App, AppName, LLoadString(IDS_PORTABLE_Q), LLoadString(IDS_HELP), LLoadString(IDS_DESKTOP), LLoadString(IDS_PORTABLE)); Dlg->SetButtonCallback(1, [this](auto idx) { App->LaunchHelp("install.html"); }); Dlg->DoModal([callback](auto dlg, auto Btn) { if (Btn == 1) { // Help LAssert(!"Help btn should use callback."); } else if (Btn == 2) { // Desktop if (callback) callback(LOptionsFile::DesktopMode); } else if (Btn == 3) { // Portable if (callback) callback(LOptionsFile::PortableMode); } else { delete dlg; LAppInst->Exit(1); } delete dlg; }); } LOptionsFile::PortableType GetInstallMode() { if (InstallMode == LOptionsFile::UnknownMode) { if (LAppInst->GetOption("portable")) { InstallMode = LOptionsFile::PortableMode; LgiTrace("Selecting portable mode based on -portable switch.\n"); } else if (LAppInst->GetOption("desktop")) { InstallMode = LOptionsFile::DesktopMode; LgiTrace("Selecting portable mode based on -desktop switch.\n"); } } if (InstallMode == LOptionsFile::UnknownMode) { bool PortableIsPossible = true; char Inst[MAX_PATH_LEN] = ""; LGetSystemPath(LSP_APP_INSTALL, Inst, sizeof(Inst)); // Do write check char Wr[MAX_PATH_LEN]; LMakePath(Wr, sizeof(Wr), Inst, "_write_test.txt"); LFile f; if (f.Open(Wr, O_WRITE)) { // Clean up f.Close(); FileDev->Delete(Wr, false); } else { // No write perms PortableIsPossible = false; } if (PortableIsPossible && LAppInst->IsElevated()) { // Check if the install is in some read only location: // e.g. c:\Program Files char Pm[MAX_PATH_LEN]; if (LGetSystemPath(LSP_USER_APPS, Pm, sizeof(Pm))) { size_t n = strlen(Pm); PortableIsPossible = _strnicmp(Pm, Inst, n) != 0; // LgiMsg(App, "%i\n%s\n%s", AppName, MB_OK, PortableIsPossible, Pm, Inst); } else LgiTrace("%s:%i - Failed to get paths.", _FL); } if (PortableIsPossible) { // Basically "ask the user" here... return LOptionsFile::UnknownMode; } else { InstallMode = LOptionsFile::DesktopMode; LgiTrace("Selecting Desktop based on lack of write permissions to install folder.\n"); } } return InstallMode; } void SetInstallMode(LOptionsFile::PortableType t) { InstallMode = t; } void DeleteCallbacks(LArray &Callbacks) { for (unsigned i=0; iGetMenu()->FindItem(Callbacks[i].Param); if (it) { it->Remove(); DeleteObj(it); } } } } }; ////////////////////////////////////////////////////////////////////////////// void UpgradeRfOption(ScribeWnd *App, const char *New, const char *Old, const char *Default) { LVariant v; /* App->GetOptions()->GetValue(New, v); if (v.Str()) { ScribePath *Path = new ScribePath(App, Old); if (Path) { char *Xml = LReadTextFile(*Path); if (Xml) { App->GetOptions()->SetValue(New, v = Xml); DeleteArray(Xml); } App->GetOptions()->DeleteValue(Old); DeleteObj(Path); } } */ if (Default && !App->GetOptions()->GetValue(New, v)) { App->GetOptions()->SetValue(New, v = Default); } } //////////////////////////////////////////////////////////////////////////// ScribeWnd::AppState ScribeWnd::ScribeState = ScribeConstructing; /* * This constructor is a little convoluted, but the basic idea is this: * * - Do some basic init. * - Attempt to load the options (could make portable/desktop mode clear) * - If the portable/desktop mode is unclear ask the user. * - Call AppConstruct1. * - If the UI language is not known, ask the user. * - Call AppConstruct2. * * Each time a dialog is needed the rest of the code needs to be in a callable function. * * It's important to note that the ScribeWnd::OnCreate method needs to be called after * the system Handle() is created, and after any dialogs in the ScribeWnd::ScribeWnd * constructor have finished. */ ScribeWnd::ScribeWnd() : BayesianFilter(this), CapabilityInstaller("Scribe", ScribeVer, "http://memecode.com/components/lookup.php", ScribeTempPath()), TrayIcon(this) { if (_Lock) _Lock->SetName("ScribeWnd"); // init some variables LApp::ObjInstance()->AppWnd = this; LCharsetSystem::Inst()->DetectCharset = ::DetectCharset; d = new ScribeWndPrivate(this); ScribeIpc = new LSharedMemory("Scribe", SCRIBE_INSTANCE_MAX * sizeof(ScribeIpcInstance)); if (ScribeIpc && ScribeIpc->GetPtr()) { ScribeIpcInstance *InstLst = (ScribeIpcInstance*) ScribeIpc->GetPtr(); for (int i=0; iMagic = SCRIBE_INSTANCE_MAGIC; ThisInst->Pid = LProcessId(); // LgiTrace("Install Scribe pid=%i to pos=%i\n", LProcessId(), i); } } } else DeleteObj(ScribeIpc); #ifndef WIN32 printf("%s\n", GetFullAppName(true).Get()); #endif auto Type = d->GetInstallMode(); if (Type == LOptionsFile::UnknownMode) { // This may make the mode more clear... if (LoadOptions()) Type = d->GetInstallMode(); } if (Type == LOptionsFile::UnknownMode) { d->AskUserForInstallMode([this](auto selectedMode) { d->SetInstallMode(selectedMode); if (!d->Options) d->Options.Reset(new LOptionsFile(selectedMode, OptionsFileName)); Construct1(); }); } else Construct1(); } void ScribeWnd::Construct1() { if (!d->Options && !LoadOptions()) { ScribeState = ScribeExiting; return; } ScribeOptionsDefaults(d->Options); LVariant GlyphSub; if (GetOptions()->GetValue(OPT_GlyphSub, GlyphSub)) { bool UseGlyphSub = GlyphSub.CastInt32() != 0; LSysFont->SubGlyphs(UseGlyphSub); LSysBold->SubGlyphs(UseGlyphSub); LFontSystem::Inst()->SetDefaultGlyphSub(UseGlyphSub); } else { GetOptions()->SetValue(OPT_GlyphSub, GlyphSub = LFontSystem::Inst()->GetDefaultGlyphSub()); } { // Limit the size of the 'Scribe.txt' log file char p[MAX_PATH_LEN]; if (LgiTraceGetFilePath(p, sizeof(p))) { int64 Sz = LFileSize(p); #define MiB * 1024 * 1024 if (Sz > (3 MiB)) FileDev->Delete(p); } } // Process pre-UI options LVariant SizeAdj; int SzAdj = SizeAdj.CastInt32(); if (GetOptions()->GetValue(OPT_UiFontSize, SizeAdj) && (SzAdj = SizeAdj.CastInt32()) >= 0 && SzAdj < 5) { SzAdj -= 2; if (SzAdj) { int Pt = LSysFont->PointSize(); LSysFont->PointSize(Pt + SzAdj); LSysFont->Create(); LSysBold->PointSize(Pt + SzAdj); LSysBold->Create(); LFont *m = LMenu::GetFont(); if (m) { m->PointSize(m->PointSize() + SzAdj); m->Create(); } } } else { GetOptions()->SetValue(OPT_UiFontSize, SizeAdj = 2); } // Resources and languages SetLanguage(); // If no language set... LVariant LangId; if (!GetOptions()->GetValue(OPT_UiLanguage, LangId)) { // Ask the user... auto Dlg = new LanguageDlg(this); if (!Dlg->Ok) { delete Dlg; LgiMsg(this, "Failed to create language selection dialog.", "Scribe Error"); ScribeState = ScribeExiting; LCloseApp(); } else { Dlg->DoModal([this, Dlg](auto dlg, auto id) { if (id) { // Set the language in the options file LVariant v; GetOptions()->SetValue(OPT_UiLanguage, v = Dlg->Lang.Get()); // Reload the resource file... to get the new lang. LResources *Cur = LgiGetResObj(false); DeleteObj(Cur); SetLanguage(); Construct2(); } else // User canceled { ScribeState = ScribeExiting; LCloseApp(); } delete dlg; }); } } else Construct2(); } void ScribeWnd::Construct2() { #if 1 auto CurRes = LgiGetResObj(false); LVariant Theme; if (CurRes && GetOptions()->GetValue(OPT_Theme, Theme)) { auto Paths = ScribeThemePaths(); auto NoTheme = LLoadString(IDS_DEFAULT); if (Theme.Str() && Stricmp(NoTheme, Theme.Str())) { for (auto p: Paths) { LFile::Path Inst(p); Inst += Theme.Str(); if (Inst.Exists()) { CurRes->SetThemeFolder(Inst); d->Static->OnSystemColourChange(); break; } } } } #endif LoadCalendarStringTable(); ZeroObj(DefaultFolderNames); DefaultFolderNames[FOLDER_INBOX] = LLoadString(IDS_FOLDER_INBOX, "Inbox"); DefaultFolderNames[FOLDER_OUTBOX] = LLoadString(IDS_FOLDER_OUTBOX, "Outbox"); DefaultFolderNames[FOLDER_SENT] = LLoadString(IDS_FOLDER_SENT, "Sent"); DefaultFolderNames[FOLDER_TRASH] = LLoadString(IDS_FOLDER_TRASH, "Trash"); DefaultFolderNames[FOLDER_CONTACTS] = LLoadString(IDS_FOLDER_CONTACTS, "Contacts"); DefaultFolderNames[FOLDER_TEMPLATES] = LLoadString(IDS_FOLDER_TEMPLATES, "Templates"); DefaultFolderNames[FOLDER_FILTERS] = LLoadString(IDS_FOLDER_FILTERS, "Filters"); DefaultFolderNames[FOLDER_CALENDAR] = LLoadString(IDS_FOLDER_CALENDAR, "Calendar"); DefaultFolderNames[FOLDER_GROUPS] = LLoadString(IDS_FOLDER_GROUPS, "Groups"); DefaultFolderNames[FOLDER_SPAM] = LLoadString(IDS_SPAM, "Spam"); LStringPipe RfXml; RfXml.Print(DefaultRfXml, LLoadString(IDS_ORIGINAL_MESSAGE), LLoadString(FIELD_TO), LLoadString(FIELD_FROM), LLoadString(FIELD_SUBJECT), LLoadString(IDS_DATE)); { LAutoString Xml(RfXml.NewStr()); UpgradeRfOption(this, OPT_TextReplyFormat, "ReplyXml", Xml); UpgradeRfOption(this, OPT_TextForwardFormat, "ForwardXml", Xml); } LFontType t; if (t.GetSystemFont("small")) { d->PreviewFont = t.Create(); if (d->PreviewFont) { #if defined WIN32 d->PreviewFont->PointSize(8); #endif } } MoveOnScreen(); // Load global graphics LoadImageResources(); // Load time threads // Window name Name(AppName); SetSnapToEdge(true); ClearTempPath(); #if WINNATIVE SetStyle(GetStyle() & ~WS_VISIBLE); SetExStyle(GetExStyle() & ~WS_EX_ACCEPTFILES); CreateClassW32(AppName, LoadIcon(LProcessInst(), MAKEINTRESOURCE(IDI_APP))); #endif #if defined LINUX SetIcon("About64px.png"); LFinishXWindowsStartup(this); #endif ScribeState = ScribeConstructed; OnCreate(); } void ScribeWnd::Construct3() { if (ScribeState == ScribeConstructing) { // Constructor is still running, probably showing some UI. // Don't complete setup at this point. return; } // Load the styles LResources::StyleElement(this); // Main menu Menu = new LMenu(AppName); if (Menu) { Menu->Attach(this); if (Menu->Load(this, "ID_MENU", GetUiTags())) { LAssert(ImageList != NULL); Menu->SetImageList(ImageList, false); auto IdentityItem = Menu->FindItem(IDM_NO_IDENTITIES); if (IdentityItem) { IdentityMenu = IdentityItem->GetParent(); } CmdSend.MenuItem = Menu->FindItem(IDM_SEND_MAIL); auto NewMailMenu = Menu->FindItem(IDM_NEW_EMAIL); if (NewMailMenu) { MailMenu = NewMailMenu->GetParent(); } LVariant v; WorkOffline = Menu->FindItem(IDM_WORK_OFFLINE); if (WorkOffline && GetOptions()->GetValue(OPT_WorkOffline, v)) { WorkOffline->Checked(v.CastInt32() != 0); } if ((d->DisableUserFilters = Menu->FindItem(IDM_FILTERS_DISABLE))) { if (GetOptions()->GetValue(OPT_DisableUserFilters, v)) { d->DisableUserFilters->Checked(v.CastInt32() != 0); } } #if RUN_STARTUP_SCRIPTS // Run scripts in './Scripts' folder char s[MAX_PATH_LEN]; LMakePath(s, sizeof(s), ScribeResourcePath(), "scripts"); if (!LDirExists(s)) LMakePath(s, sizeof(s), LGetSystemPath(LSP_APP_INSTALL), #if defined(LINUX) || defined(WINDOWS) "..\\" #endif "scripts"); if (!LDirExists(s)) LgiTrace("%s:%i - Error: the scripts folder '%s' doesn't exist.\n", _FL, s); else { bool ErrorDsp = false; LDirectory Dir; for (int b = Dir.First(s); b; b = Dir.Next()) { if (Dir.IsDir()) continue; char *Ext = LGetExtension(Dir.GetName()); if (!Ext || _stricmp(Ext, "script") != 0) continue; Dir.Path(s, sizeof(s)); LStringPipe Log; char *Source = LReadTextFile(s); if (Source) { LScript *Cur = new LScript; if (Cur) { char Msg[256]; d->CurrentScripts.Add(Cur); LScribeScript::Inst->GetLog()->Write(Msg, sprintf_s(Msg, sizeof(Msg), "Compiling '%s'...\n", Dir.GetName())); LCompiler c; if (c.Compile( Cur->Code, d->Engine->GetSystemContext(), LScribeScript::Inst, s, Source, NULL)) { LFunctionInfo *Main = Cur->Code->GetMethod("Main"); if (Main) { LVirtualMachine Vm(d); LScriptArguments Args(&Vm); Args.New() = new LVariant((LDom*)this); d->Scripts.Add(Cur); if (Vm.ExecuteFunction( Cur->Code, Main, Args, LScribeScript::Inst->GetLog()) && Args.GetReturn()->CastInt32()) { d->CurrentScripts.Delete(Cur, true); Cur = NULL; } else { LgiTrace("Error: Script's main failed (%s)\n", Cur->Code->GetFileName()); if (Cur->Callbacks.Length()) d->DeleteCallbacks(Cur->Callbacks); d->Scripts.Delete(Cur); } Args.DeleteObjects(); } } else if (!ErrorDsp) { ErrorDsp = true; OnScriptCompileError(Source, NULL); } if (Cur) { d->CurrentScripts.Delete(Cur, true); DeleteObj(Cur); } } DeleteArray(Source); } } } #endif #define EnableItem(id, en) { auto i = Menu->FindItem(id); if (i) i->Enabled(en); } #define SetMenuIcon(id, ico) { auto i = Menu->FindItem(id); if (i) i->Icon(ico); } EnableItem(IDM_IMPORT_OUTLOOK_ITEMS, true); // SetMenuIcon(IDM_OPEN_FOLDERS, ICON_OPEN_FOLDER); SetMenuIcon(IDM_OPTIONS, ICON_OPTIONS); SetMenuIcon(IDM_SECURITY, ICON_LOCK); SetMenuIcon(IDM_CUT, ICON_CUT); SetMenuIcon(IDM_COPY, ICON_COPY); SetMenuIcon(IDM_PASTE, ICON_PASTE); SetMenuIcon(IDM_LAYOUT1, ICON_LAYOUT1); SetMenuIcon(IDM_LAYOUT2, ICON_LAYOUT2); SetMenuIcon(IDM_LAYOUT3, ICON_LAYOUT3); SetMenuIcon(IDM_LAYOUT4, ICON_LAYOUT4); SetMenuIcon(IDM_NEW_EMAIL, ICON_UNSENT_MAIL); SetMenuIcon(IDM_SET_READ, ICON_READ_MAIL); SetMenuIcon(IDM_SET_UNREAD, ICON_UNREAD_MAIL); SetMenuIcon(IDM_NEW_CONTACT, ICON_CONTACT); SetMenuIcon(IDM_NEW_GROUP, ICON_CONTACT_GROUP); SetMenuIcon(IDM_REPLY, ICON_FLAGS_REPLY); SetMenuIcon(IDM_REPLY_ALL, ICON_FLAGS_REPLY); SetMenuIcon(IDM_FORWARD, ICON_FLAGS_FORWARD); SetMenuIcon(IDM_BOUNCE, ICON_FLAGS_BOUNCE); SetMenuIcon(IDM_NEW_FILTER, ICON_FILTER); SetMenuIcon(IDM_FILTER_CURRENT_FOLDER, ICON_FOLDER_FILTERS); SetMenuIcon(IDM_MEMECODE, ICON_LINK); SetMenuIcon(IDM_HOMEPAGE, ICON_LINK); SetMenuIcon(IDM_SCRIBE_FAQ, ICON_LINK); SetMenuIcon(IDM_INSCRIBE_LINK, ICON_LINK); SetMenuIcon(IDM_VERSION_HISTORY, ICON_LINK); SetMenuIcon(IDM_DEBUG_INFO, ICON_LINK); SetMenuIcon(IDM_TUTORIALS, ICON_LINK); SetMenuIcon(IDM_FEEDBACK, ICON_UNREAD_MAIL); SetMenuIcon(IDM_HELP, ICON_HELP); LMenuItem *mi; if ( GetOptions()->GetValue(OPT_EditControl, v) && (mi = Menu->FindItem(IDM_HTML_EDITOR)) ) mi->Checked(v.CastInt32() != 0); Menu->SetPrefAndAboutItems(IDM_OPTIONS, IDM_ABOUT); } } // Initialize user interface SetupUi(); // Get some of the base submenu pointers. These are needed // for SetupAccounts to work correctly, e.g. populate the // send/receive/preview submenus. Folders need to be loaded // before this for the templates folder BuildDynMenus(); // Load accounts SetupAccounts(); // Recursively load folder tree LoadFolders([this](auto status) { // Redo it for the templates... now that load folders has completed. BuildDynMenus(); if (ScribeState == ScribeExiting) return; // Process command line OnCommandLine(); // Update the templates sub-menu now that the folders are loaded BuildDynMenus(); // Check registry settings SetDefaultHandler(); // Run on load scripts... LArray OnLoadCallbacks; if (GetScriptCallbacks(LOnLoad, OnLoadCallbacks)) { for (auto r: OnLoadCallbacks) { LVirtualMachine Vm; LScriptArguments Args(&Vm); Args.New() = new LVariant(this); ExecuteScriptCallback(*r, Args); Args.DeleteObjects(); } } ScribeState = ScribeRunning; }); } void ScribeWnd::SetLanguage() { LVariant LangId; if (GetOptions()->GetValue(OPT_UiLanguage, LangId)) { // Set the language to load... LAppInst->SetConfig("Language", LangId.Str()); } LResources::SetLoadStyles(true); // Load the resources (with the current lang) if (!LgiGetResObj(true, "Scribe")) { LgiMsg(NULL, "The resource file 'Scribe.lr8' is missing.", AppName); ScribeState = ScribeExiting; LCloseApp(); } } ScribeWnd::~ScribeWnd() { LAppInst->AppWnd = 0; SearchView = NULL; ScribeState = ScribeExiting; LScribeScript::Inst->ShowScriptingWindow(false); // Other cleanup... ClearTempPath(); ShutdownIpc(); SetPulse(); // Save anything thats still dirty in the folders... // just in case we crash during the shutdown phase. ScribeFolder *Cur = GetCurrentFolder(); if (Cur) Cur->SerializeFieldWidths(); SaveDirtyObjects(5000); // Tell the UI not to reference anything in the folders if (PreviewPanel) { PreviewPanel->OnThing(0, false); } Mail::NewMailLst.Empty(); // ~AccountStatusItem references the account list... must be before we // delete the accounts. DeleteObj(StatusPanel); // ~Accountlet needs to reference the root container... so // it has to go before unloading of folders. Accounts.DeleteObjects(); UnLoadFolders(); DeleteObj(PreviewPanel); SaveOptions(); DeleteObj(Commands); DeleteObj(d->PreviewFont); DeleteObj(d->SubSplit); DeleteObj(Splitter); MailList = NULL; CmdSend.ToolButton = NULL; CmdReceive.ToolButton = NULL; CmdPreview.ToolButton = NULL; CmdSend.MenuItem = NULL; CmdReceive.MenuItem = NULL; CmdPreview.MenuItem = NULL; // This could be using the OpenSSL library for HTTPS connections. So // close it before calling EndSSL. DeleteObj(d->ImageLoader); // This has to be after we close all the accounts... otherwise // they might still be using SSL functions, e.g. an IMAP/SSL connect. EndSSL(); DeleteObj(d); } LString ScribeWnd::GetResourceFile(SribeResourceType Type) { auto File = d->ResFiles.Find(Type); if (!File) LgiTrace("%s:%i - No file for resource type %i\n", _FL, Type); return File; } void ScribeWnd::LoadImageResources() { auto Res = LgiGetResObj(); LString::Array Folders; if (Res) { auto p = Res->GetThemeFolder(); if (p) Folders.Add(p); } Folders.Add(ScribeResourcePath()); for (auto p: Folders) { LDirectory Dir; LgiTrace("%s:%i - Loading resource folder '%s'\n", _FL, p.Get()); for (auto b = Dir.First(p); b; b = Dir.Next()) { if (Dir.IsDir()) continue; auto Name = Dir.GetName(); if (MatchStr("Toolbar-*.png", Name)) { if (!d->ResFiles.Find(ResToolbarFile)) d->ResFiles.Add(ResToolbarFile, Dir.FullPath()); } else if (MatchStr("xgate-icons-*.png", Name)) d->ResFiles.Add(ResToolbarFile, Dir.FullPath()); else if (MatchStr("Icons-*.png", Name)) d->ResFiles.Add(ResIconsFile, Dir.FullPath()); } } ToolbarImgs.Reset(LLoadImageList(GetResourceFile(ResToolbarFile))); ImageList.Reset(LLoadImageList(GetResourceFile(ResIconsFile))); if (!ImageList) LgiTrace("%s:%i - Failed to load toolbar image ('xgate-icons-32.png' or 'Toolbar-24.png')\n", _FL); } int ScribeWnd::GetEventHandle() { return d->AppWndHnd; } void ScribeWnd::OnCloseInstaller() { d->Bar = NULL; if (InThread()) { PourAll(); } else LAssert(0); } void ScribeWnd::OnInstall(CapsHash *Caps, bool Status) { } bool ScribeWnd::NeedsCapability(const char *Name, const char *Param) { #if DEBUG_CAPABILITIES LgiTrace("ScribeWnd::NeedsCapability(%s, %s)\n", Name, Param); #endif if (!InThread()) { #if DEBUG_CAPABILITIES LgiTrace("%s:%i - Posting M_NEEDS_CAP\n", _FL); #endif PostEvent(M_NEEDS_CAP, (LMessage::Param)NewStr(Name), (LMessage::Param)NewStr(Param)); } else { if (!Name) return false; if (d->MissingCaps.Find(Name)) { #if DEBUG_CAPABILITIES LgiTrace("%s:%i - Already in MissingCaps\n", _FL); #endif return true; } d->MissingCaps.Add(Name, true); LStringPipe MsgBuf(256); int i = 0; // const char *k; // for (bool b=d->MissingCaps.First(&k); b; b=d->MissingCaps.Next(&k), i++) for (auto k : d->MissingCaps) { MsgBuf.Print("%s%s", i?", ":"", k.key); } LVariant Actions; if (stristr(Name, "OpenSSL")) { MsgBuf.Print(LLoadString(IDS_ERROR_SERVER_CONNECT)); if (Param) MsgBuf.Print("\n%s", Param); Actions.Add(new LVariant(LLoadString(IDS_INSTALL))); } else if (stristr(Name, "Registry")) { MsgBuf.Print(LLoadString(IDS_ERROR_REG_WRITE)); Actions.Add(new LVariant(LLoadString(IDS_DONT_SHOW_AGAIN))); } else if (stristr(Name, "SpellingDictionary")) { MsgBuf.Print(LLoadString(IDS_ERROR_NEED_INSTALL), Param); Actions.Add(new LVariant(LLoadString(IDS_DOWNLOAD))); } Actions.Add(new LVariant(LLoadString(IDS_OK))); #if DEBUG_CAPABILITIES LgiTrace("%s:%i - Actions.Length()=%i, Bar=%p\n", _FL, Actions.Length(), d->Bar); #endif if (Actions.Length()) { LAutoString Msg(MsgBuf.NewStr()); // Check the script hook here... bool ShowInstallBar = true; LArray Callbacks; if (GetScriptCallbacks(LBeforeInstallBar, Callbacks)) { for (unsigned i=0; iCastInt32()) ShowInstallBar = false; else Msg.Reset(TheMsg.ReleaseStr()); } } } // Now create the capability install bar... if (!d->Bar && ShowInstallBar && Actions.Type == GV_LIST) { // FYI Capabilities are handled in ScribeWnd::StartAction. LArray Act; for (auto v : *Actions.Value.Lst) Act.Add(v->Str()); d->Bar = new MissingCapsBar(this, &d->MissingCaps, Msg, this, Act); AddView(d->Bar, 2); AttachChildren(); OnPosChange(); } } } return true; } LAutoString ScribeWnd::GetDataFolder() { LVariant v; GetOptions()->GetValue(OPT_IsPortableInstall, v); char p[MAX_PATH_LEN]; if (LGetSystemPath(v.CastInt32() ? LSP_APP_INSTALL : LSP_APP_ROOT, p, sizeof(p))) { if (!LDirExists(p)) FileDev->CreateFolder(p); return LAutoString(NewStr(p)); } else LgiTrace("%s:%i - LgiGetSystemPath failed (portable=%i).\n", _FL, v.CastInt32()); return LAutoString(); } LScriptEngine *ScribeWnd::GetScriptEngine() { return d->Engine; } LScriptCallback ScribeWnd::GetCallback(const char *CallbackMethodName) { LScriptCallback Cb; auto Cur = d->CurrentScript(); if (Cur && Cur->Code) { Cb.Script = Cur; Cb.Func = Cur->Code->GetMethod(CallbackMethodName); } if (!Cb.Func) { for (auto s: d->Scripts) { Cb.Script = s; if ((Cb.Func = s->Code->GetMethod(CallbackMethodName))) break; } } return Cb; } bool ScribeWnd::RegisterCallback(LScriptCallbackType Type, LScriptArguments &Args) { if (!d->CurrentScript()) { LgiTrace("%s:%i - No current script.\n", _FL); return false; } char *Fn = Args[1]->Str(); LScriptCallback Cb = GetCallback(Fn); if (!Cb.Func) { LgiTrace("%s:%i - No callback '%s'.\n", _FL, Fn); return false; } switch (Type) { case LToolsMenu: { char *Menu = Args[0]->Str(); auto Cur = d->CurrentScript(); if (!Menu || !Fn || !Cur) { LgiTrace("%s:%i - menu=%s, fn=%s.\n", _FL, Menu, Fn); return false; } LScriptCallback &c = Cur->Callbacks.New(); c = Cb; c.Type = Type; c.Param = d->NextToolMenuId; LMenuItem *Tools = GetMenu()->FindItem(IDM_TOOLS_MENU); auto ToolSub = Tools ? Tools->Sub() : 0; if (ToolSub) { if (d->NextToolMenuId == IDM_TOOL_SCRIPT_BASE) { ToolSub->AppendSeparator(); } ToolSub->AppendItem(Menu, c.Param, true); d->NextToolMenuId++; } break; } case LThingContextMenu: case LFolderContextMenu: case LThingUiToolbar: case LMailOnBeforeSend: case LMailOnAfterReceive: case LApplicationToolbar: case LBeforeInstallBar: case LInstallComponent: case LOnTimer: case LRenderMail: case LOnLoad: { auto Cur = d->CurrentScript(); LAssert(d->Scripts.HasItem(Cur)); LScriptCallback &c = Cur->Callbacks.New(); c = Cb; c.Type = Type; if (Args.Length() > 2) c.Data = *Args[2]; break; } default: { LAssert(!"Not a known callback type"); return false; } } return true; } bool ScribeWnd::GetScriptCallbacks(LScriptCallbackType Type, LArray &Callbacks) { for (auto s: d->Scripts) { for (auto &c: s->Callbacks) { if (c.Type == Type) Callbacks.Add(&c); } } return Callbacks.Length() > 0; } bool ScribeWnd::ExecuteScriptCallback(LScriptCallback &c, LScriptArguments &Args, bool ReturnArgs) { if (!c.Func || !c.Script) return false; // Setup LVirtualMachine Vm(d); d->CurrentScripts.Add(c.Script); // Call the method bool Status = Vm.ExecuteFunction( c.Script->Code, c.Func, Args, LScribeScript::Inst->GetLog(), ReturnArgs ? &Args : NULL) != ScriptError; // Cleanup d->CurrentScripts.PopLast(); return Status; } LStream *ScribeWnd::ShowScriptingConsole() { auto Item = Menu->FindItem(IDM_SCRIPTING_CONSOLE); if (Item) { Item->Checked(!Item->Checked()); LScribeScript::Inst->ShowScriptingWindow(Item->Checked()); LVariant v; GetOptions()->SetValue(OPT_ShowScriptConsole, v = Item->Checked()); } return LScribeScript::Inst->GetLog(); } LOptionsFile::PortableType ScribeWnd::GetPortableType() { return d->GetInstallMode(); } void ScribeWnd::RemoteContent_AddSender(const char *Addr, bool WhiteList) { if (!Addr) return; auto Opt = WhiteList ? OPT_RemoteContentWhiteList : OPT_RemoteContentBlackList; LVariant v; GetOptions()->GetValue(Opt, v); // Not an error if not there... auto existing = LString(v.Str()).SplitDelimit(" ,\r\n"); for (auto p: existing) { if (MatchStr(p, Addr)) { LgiTrace("%s:%i - '%s' is already in '%s'\n", _FL, Addr, Opt); return; // Already in list... } } existing.SetFixedLength(false); existing.Add(Addr); auto updated = LString("\n").Join(existing); GetOptions()->SetValue(Opt, v = updated.Get()); LgiTrace("%s:%i - Added '%s' to '%s'\n", _FL, Addr, Opt); d->RemoteContent_Init = false; } ScribeRemoteContent ScribeWnd::RemoteContent_GetSenderStatus(const char *Addr) { if (!d->RemoteContent_Init) { LVariant v; if (GetOptions()->GetValue(OPT_RemoteContentWhiteList, v)) d->RemoteWhiteLst = LString(v.Str()).SplitDelimit(" ,\r\n"); if (GetOptions()->GetValue(OPT_RemoteContentBlackList, v)) d->RemoteBlackLst = LString(v.Str()).SplitDelimit(" ,\r\n"); d->RemoteContent_Init = true; } for (auto p: d->RemoteWhiteLst) if (MatchStr(p, Addr)) return RemoteAlwaysLoad; for (auto p: d->RemoteBlackLst) if (MatchStr(p, Addr)) return RemoteNeverLoad; return RemoteDefault; } void ScribeWnd::RemoteContent_ClearCache() { d->RemoteWhiteLst.Empty(); d->RemoteBlackLst.Empty(); d->RemoteContent_Init = false; } void ScribeWnd::OnSpellerSettingChange() { // Kill the current thread d->SpellerThread.Reset(); // Setup the new thread LSpellCheck *t = GetSpellThread(); if (t) { // Trigger an install if needed t->Check(d->AppWndHnd, "thisisamispeltword", 0, 18); } } bool ScribeWnd::SetSpellThreadParams(LSpellCheck *Thread) { if (!Thread) return false; LVariant Lang, Dict; GetOptions()->GetValue(OPT_SpellCheckLanguage, Lang); GetOptions()->GetValue(OPT_SpellCheckDictionary, Dict); LAutoPtr Params(new LSpellCheck::Params); if (!Params) return false; Params->IsPortable = GetPortableType(); Params->OptionsPath = GetOptions()->GetFile(); Params->Lang = Lang.Str(); Params->Dict = Dict.Str(); Params->CapTarget = this; Thread->SetParams(Params); return true; } LSpellCheck *ScribeWnd::CreateSpellObject() { LVariant PrefAspell; GetOptions()->GetValue(OPT_PreferAspell, PrefAspell); LAutoPtr Obj; if (PrefAspell.CastInt32()) Obj = CreateAspellObject(); #if defined(MAC) if (!Obj) Obj = CreateAppleSpellCheck(); #elif defined(WINDOWS) LArray Ver; int Os = LGetOs(&Ver); if ( !Obj && (Os == LGI_OS_WIN32 || Os == LGI_OS_WIN64) && ( Ver.Length() > 1 && ( Ver[0] > 6 || (Ver[0] == 6 && Ver[1] > 1) ) ) ) Obj = CreateWindowsSpellCheck(); #endif if (!Obj) Obj = CreateAspellObject(); SetSpellThreadParams(Obj); return Obj.Release(); } LSpellCheck *ScribeWnd::GetSpellThread(bool OverrideOpt) { LVariant Use; if (OverrideOpt) Use = true; else GetOptions()->GetValue(OPT_SpellCheck, Use); #if USE_SPELLCHECKER if ((Use.CastInt32() != 0) ^ (d->SpellerThread.Get() != 0)) d->SpellerThread.Reset(Use.CastInt32() ? CreateSpellObject() : NULL); #endif return d->SpellerThread; } LAutoString ScribeWnd::GetHttpProxy() { LAutoString Proxy; LVariant v; if (GetOptions()->GetValue(OPT_HttpProxy, v) && ValidStr(v.Str())) { Proxy.Reset(v.ReleaseStr()); } else { LProxyUri p; if (p.sHost) Proxy.Reset(NewStr(p.ToString())); } return Proxy; } InstallProgress *ScribeWnd::StartAction(MissingCapsBar *Bar, LCapabilityTarget::CapsHash *Components, const char *ActionParam) { if (!ActionParam) { LgiTrace("%s:%i - No action supplied.\n", _FL); return NULL; } LArray Callbacks; LVariant Action(ActionParam); if (GetScriptCallbacks(LInstallComponent, Callbacks)) { bool StartInstall = true; for (unsigned i=0; iCastInt32()) StartInstall = false; } } if (!Action.Str()) { LgiTrace("%s:%i - GInstallComponent removed action name.\n", _FL); return NULL; } if (!StartInstall) { LgiTrace("%s:%i - GInstallComponent script canceled install of '%s'.\n", _FL, ActionParam); return NULL; } } if (!_stricmp(Action.Str(), LLoadString(IDS_OK))) { // Do nothing d->MissingCaps.Empty(); } else if (!_stricmp(Action.Str(), LLoadString(IDS_DONT_SHOW_AGAIN))) { // Turn off registering as a client. LVariant No(false); GetOptions()->SetValue(OPT_RegisterWindowsClient, No); GetOptions()->SetValue(OPT_CheckDefaultEmail, No); } else if (!_stricmp(Action.Str(), LLoadString(IDS_INSTALL))) { #ifdef WINDOWS bool IsSsl = false; for (auto c: *Components) { if (!_stricmp(c.key, "openssl")) { IsSsl = true; break; } } if (IsSsl) { LString s; s.Printf(LLoadString(IDS_WINDOWS_SSL_INSTALL), LGetOsName()); auto q = new LAlert(this, AppName, s, "Open Website", LLoadString(IDS_CANCEL)); q->DoModal([this, q](auto dlg, auto id) { switch (id) { case 1: LExecute("https://slproweb.com/products/Win32OpenSSL.html"); break; default: break; } delete dlg; }); return NULL; } #endif return CapabilityInstaller::StartAction(Bar, Components, Action.Str()); } else if (!_stricmp(Action.Str(), LLoadString(IDS_SHOW_CONSOLE))) { ShowScriptingConsole(); } else if (!_stricmp(Action.Str(), LLoadString(IDS_OPEN_SOURCE))) { if (d->ErrSource) LExecute(d->ErrSource); else if (d->ErrFilter) d->ErrFilter->DoUI(); d->ErrSource.Empty(); d->ErrFilter = NULL; } else if ( !Stricmp(Action.Str(), LLoadString(IDS_SHOW_REMOTE_CONTENT)) || !Stricmp(Action.Str(), LLoadString(IDS_ALWAYS_SHOW_REMOTE_CONTENT))) { auto c = Components->begin(); LWindow *w = Bar->GetWindow(); if ((*c).key && !Stricmp((*c).key, "RemoteContent") && w) { LVariant Ret, Always(!Stricmp(Action.Str(), LLoadString(IDS_ALWAYS_SHOW_REMOTE_CONTENT))); LArray Args; Args[0] = &Always; w->CallMethod(DomToStr(SdShowRemoteContent), &Ret, Args); } } else if (!_stricmp(Action.Str(), LLoadString(IDS_DOWNLOAD))) { auto t = GetSpellThread(); if (t) t->InstallDictionary(); else LgiTrace("%s:%i - No spell thread.\n", _FL); } else LAssert(!"Unknown action."); return NULL; } HttpImageThread *ScribeWnd::GetImageLoader() { if (!d->ImageLoader) { LAutoString Proxy = GetHttpProxy(); d->ImageLoader = new HttpImageThread(this, Proxy, 0); } return d->ImageLoader; } char *ScribeWnd::GetUiTags() { if (!d->UiTags) { char UiTags[256] = "inscribe" #if defined WIN32 " win32" #elif defined LINUX " linux" #elif defined MAC " mac" #endif ; LVariant Tags; if (!GetOptions()) { LAssert(!"Where is the options?"); } else if (GetOptions()->GetValue("tags", Tags)) { size_t Len = strlen(UiTags); sprintf_s(UiTags+Len, sizeof(UiTags)-Len, " %s", Tags.Str()); } d->UiTags.Reset(NewStr(UiTags)); } return d->UiTags; } void ScribeWnd::OnCreate() { // LgiTrace("ScribeWnd::OnCreate. ScribeState=%i\n", ScribeState); if (IsAttached() && ScribeState == ScribeConstructed) { ScribeState = ScribeInitializing; Construct3(); } } ScribeAccount *ScribeWnd::GetAccountByEmail(const char *Email) { if (!Email) return NULL; for (auto a : *GetAccounts()) { LVariant e = a->Identity.Email(); if (e.Str() && !_stricmp(e.Str(), Email)) { return a; } } return 0; } ScribeAccount *ScribeWnd::GetAccountById(int Id) { for (auto a : *GetAccounts()) { if (a->Receive.Id() == Id) { return a; } } return 0; } const char *ScribeWnd::EditCtrlMimeType() { LVariant Html; GetOptions()->GetValue(OPT_EditControl, Html); return Html.CastInt32() ? sTextHtml : sTextPlain; } LAutoString ScribeWnd::GetReplyXml(const char *MimeType) { bool IsHtml = MimeType && !_stricmp(MimeType, sTextHtml); LVariant s; GetOptions()->GetValue(IsHtml ? OPT_HtmlReplyFormat : OPT_TextReplyFormat, s); return LAutoString(s.ReleaseStr()); } LAutoString ScribeWnd::GetForwardXml(const char *MimeType) { bool IsHtml = MimeType && !_stricmp(MimeType, sTextHtml); LVariant s; GetOptions()->GetValue(IsHtml ? OPT_HtmlForwardFormat : OPT_TextForwardFormat, s); return LAutoString(s.ReleaseStr()); } LVmCallback *ScribeWnd::GetDebuggerCallback() { return d; } GpgConnector *ScribeWnd::GetGpgConnector() { if (!d->GpgInst) { if (!GpgConnector::IsInstalled()) return NULL; d->GpgInst.Reset(new GpgConnector()); } return d->GpgInst; } LFont *ScribeWnd::GetPreviewFont() { return d->PreviewFont; } bool ScribeWnd::IsValid() { #if 0 try { for (ScribeAccount *a = Accounts.First(); a; a = Accounts.Next()) { } } catch(...) { return false; } #endif return true; } bool ScribeWnd::ShutdownIpc() { // Remove our instance from the shared memory if (ScribeIpc && ScribeIpc->GetPtr()) { ScribeIpcInstance *InstLst = (ScribeIpcInstance*) ScribeIpc->GetPtr(); if (ThisInst) memset(ThisInst, 0, sizeof(*ThisInst)); int c = 0; for (int i=0; iDestroy(); } ThisInst = 0; DeleteObj(ScribeIpc); return true; } bool ScribeWnd::GetVariant(const char *Name, LVariant &Value, const char *Array) { ScribeDomType Fld = StrToDom(Name); switch (Fld) { case SdQuote: // Type: String { return GetOptions()->GetValue(OPT_QuoteReplyStr, Value); } case SdName: // Type: String { Value = AppName; break; } case SdHome: // Type: String { Value = LGetExePath().Get(); break; } case SdNow: // Type: String { LDateTime Now; Now.SetNow(); char s[64]; Now.Get(s, sizeof(s)); Value = s; break; } case SdFolder: // Type: ScribeFolder[] // Pass system folder index or string as array parameter. { ScribeFolder *f = 0; if (!Array) return false; if (IsDigit(*Array)) { f = GetFolder(atoi(Array)); } else { f = GetFolder(Array); } if (!f) return false; Value = (LDom*)f; break; } case SdAppName: // Type: String { Value = AppName; break; } case SdCalendarToday: // Type: String { Calendar::SummaryOfToday(this, [this](auto s) { d->CalendarSummary = s; }); return d->CalendarSummary; } case SdInboxSummary: { LStringPipe p; // Iterate through the mail stores for (auto m: Folders) { if (!m.Store) continue; auto Inbox = m.Store->GetObj(FIELD_INBOX); if (!Inbox) continue; auto Unread = Inbox->GetInt(FIELD_UNREAD); auto Name = Inbox->GetStr(FIELD_FOLDER_NAME); LString Path; Path.Printf("/%s/%s", m.Name.Get(), Name); if (Unread) p.Print("%s: %i unread
", m.Name.Get(), Path.Get(), Unread); else p.Print("%s: 0 unread
", m.Name.Get()); } // And also the IMAP full folders for (auto a: Accounts) { ScribeProtocol Protocol = a->Receive.ProtocolType(); if (Protocol == ProtocolImapFull) { LDataStoreI *Store = a->Receive.GetDataStore(); if (Store) { auto Inbox = Store->GetObj(FIELD_INBOX); if (Inbox) { auto Unread = Inbox->GetInt(FIELD_UNREAD); auto Name = Inbox->GetStr(FIELD_FOLDER_NAME); auto m = a->Receive.Name(); if (m.Str()) { LString Path; Path.Printf("/%s/%s", m.Str(), Name); if (Unread) p.Print("%s: %i unread
", m.Str(), Path.Get(), Unread); else p.Print("%s: 0 unread
", m.Str()); } } } } } Value = p.NewLStr().Get(); break; } case SdExecute: // Type: String { if (!Array) return false; const char *s = Array; char *Exe = LTokStr(s); if (!Exe) return false; while (*s && *s == ' ') s++; LStringPipe Out; LSubProcess p(Exe, (char*)s); if (p.Start()) { p.Communicate(&Out); LAutoString o(Out.NewStr()); LAutoString t(TrimStr(o)); Value = t; } DeleteArray(Exe); break; } case SdBuildType: // Type: String { #ifdef _DEBUG Value = "Debug"; #else Value = "Release"; #endif break; } case SdPlatform: // Type: String { LArray Ver; LGetOs(&Ver); LString::Array Va; for (auto i: Ver) Va.New().Printf("%i", i); #if defined __GTK_H__ auto Api = "GTK3"; #elif LGI_SDL auto Api = "SDL"; #elif LGI_COCOA auto Api = "Cocoa"; #elif LGI_CARBON auto Api = "Carbon"; #elif defined WIN32 auto Api = "WinApi"; #else #error "Impl me." auto Api = "#err"; #endif LString s; s.Printf("%s, v%s, %s", LGetOsName(), LString(".").Join(Va).Get(), Api); Value = s.Get(); break; } case SdVersion: // Type: String { char Ver[32]; sprintf_s(Ver, sizeof(Ver), "v%s", ScribeVer); Value = Ver; break; } case SdBuild: // Type: String { char s[128]; sprintf_s(s, sizeof(s), "%s, %s, %ibit", __DATE__, __TIME__, (int)(sizeof(NativeInt)*8)); Value = s; break; } case SdLanguage: // Type: String { LLanguage *l = LGetLanguageId(); if (!l) return false; char s[256]; sprintf_s(s, sizeof(s), "%s \"%s\"", l->Name, l->Id); Value = s; break; } case SdString: // Type: String { if (!Array) { LAssert(!"Missing string ID"); return false; } int Id = atoi(Array); Value = LLoadString(Id); break; } case SdCurrentFolder: // Type: ScribeFolder { Value = (LDom*) GetCurrentFolder(); break; } case SdView: // Type: LView { Value = (LView*)this; break; } case SdNoContact: // Type: Contact { Value = (NoContactType*)d->NoContact; break; } case SdAccounts: { if (Array) { if (IsDigit(*Array)) { auto i = atoi(Array); if (i >= 0 && i < (ssize_t)Accounts.Length()) { Value = (LDom*)Accounts[i]; } else return false; } else { for (auto a : Accounts) { LVariant nm = a->Send.Name(); if (nm.Str() && !_stricmp(nm.Str(), Array)) { Value = (LDom*)a; break; } } } } else { Value = (int32)Accounts.Length(); } break; } case SdOptions: { Value = GetOptions(); break; } case SdMailStorePaths: { if (!Value.SetList()) return false; for (auto Ms : Folders) Value.Add(new LVariant(Ms.Path)); break; } case SdRootFolders: { if (!Value.SetList() || !Tree) return false; for (auto *i = Tree->GetChild(); i; i = i->GetNext()) { ScribeFolder *c = dynamic_cast(i); if (c) { auto p = c->GetPath(); Value.Add(new LVariant(p)); } } break; } default: { return false; } } return true; } bool ScribeWnd::CallMethod(const char *MethodName, LVariant *ReturnValue, LArray &Args) { ScribeDomType m = StrToDom(MethodName); switch (m) { case SdGrowlOnMail: // Type: (Mail Obj) { if (Args.Length() != 1) { LgiTrace("%s:%i - Wrong arg count: %i.\n", _FL, (int)Args.Length()); return false; } Mail *m = dynamic_cast(Args[0]->CastDom()); if (!m) { LgiTrace("%s:%i - Invalid object.\n", _FL); return false; } GrowlOnMail(m); break; } case SdGrowlInfo: { auto Title = Args.Length() > 0 ? Args[0] : NULL; auto Text = Args.Length() > 1 ? Args[1] : NULL; GrowlInfo(Title ? Title->Str() : NULL, Text ? Text->Str() : NULL); break; } case SdGetClipboardText: // Type: () { LClipBoard c(this); *ReturnValue = c.Text(); break; } case SdSetClipboardText: // Type: (String Text) { if (Args.Length() != 1) { LgiTrace("%s:%i - Wrong arg count: %i.\n", _FL, (int)Args.Length()); return false; } char *Str = Args[0]->CastString(); LClipBoard c(this); if (ValidStr(Str)) *ReturnValue = c.Text(Str); else *ReturnValue = c.Empty(); break; } case SdLookupContactGroup: // Type: (String GroupName) { if (Args.Length() != 1) { LgiTrace("%s:%i - Wrong arg count: %i.\n", _FL, (int)Args.Length()); return false; } ContactGroup *Grp = LookupContactGroup(this, Args[0]->Str()); *ReturnValue = dynamic_cast(Grp); break; } case SdAskUserString: // Type: (LView ParentView, String Callback, String PromptMessage[, Bool ObsurePassword[, String DefaultValue]]) { LVirtualMachine::Context Ctx; // Ctx = Args.GetVm()->SaveContext() if (!Ctx || Args.Length() < 3) { *ReturnValue = true; return true; } LView *View = Args[0]->CastView(); auto CallbackName = Args[1]->Str(); auto Prompt = Args[2]->CastString(); bool IsPassword = Args.Length() > 3 ? Args[3]->CastInt32() != 0 : false; auto Default = Args.Length() > 4 ? Args[4]->Str() : NULL; auto i = new LInput(View ? View : this, Default, Prompt, AppName, IsPassword); i->DoModal([Ctx, i, CallbackName=LString(CallbackName)](auto dlg, auto id) { if (id) { LScriptArguments Args(NULL); Args.Add(new LVariant(i->GetStr())); Ctx.Call(CallbackName, Args); Args.DeleteObjects(); } delete dlg; }); *ReturnValue = true; break; } case SdCreateAccount: // Type: () { ScribeAccount *a = new ScribeAccount(this, (int)Accounts.Length()); if (a) { if (ReturnValue) *ReturnValue = (LDom*)a; Accounts.Insert(a); a->Create(); } else return false; break; } case SdDeleteAccount: // Type: (ScribeAccount AccountToDelete) { if (Args.Length() != 1) return false; ScribeAccount *a = dynamic_cast(Args[0]->CastDom()); if (!a) { if (ReturnValue) *ReturnValue = false; } else { int Idx = (int)Accounts.IndexOf(a); if (Idx < 0 || a->IsOnline()) { if (ReturnValue) *ReturnValue = false; } else { Accounts.Delete(a); a->Delete(); delete a; // delete actual account object // Reindex remaining items so their are no gaps int i=0; auto it = Accounts.begin(); for (a = *it; a; a = *++it, i++) { a->ReIndex(i); } } } break; } case SdShowRemoteContent: { if (PreviewPanel) return PreviewPanel->CallMethod(MethodName, ReturnValue, Args); else return false; break; } case SdSearchHtml: // Type(Html, SearchExp, ResultExp) { if (Args.Length() != 3) { LgiTrace("%s:%i - SearchHtml requires 3 arguments.\n", _FL); *ReturnValue = false; return true; } auto Html = Args[0]->Str(); auto SearchExp = Args[1]->Str(); auto ResultExp = Args[2]->Str(); if (!Html || !SearchExp || !ResultExp) { LgiTrace("%s:%i - SearchHtml got non-string argument.\n", _FL); *ReturnValue = false; return true; } SearchHtml(ReturnValue, Html, SearchExp, ResultExp); return true; } case SdGetUri: // Type(UriToDownload, CallbackName) { if (Args.Length() < 2) { LgiTrace("%s:%i - GetUri requires at least 2 arguments.\n", _FL); *ReturnValue = false; return true; } auto Uri = Args[0]->Str(); auto Callback = Args[1]->Str(); LVariant *UserData = Args.Length() > 2 ? Args[2] : NULL; new ScriptDownloadContentThread(this, Uri, Callback, UserData); *ReturnValue = true; return true; } default: { LAssert(!"Unsupported method."); return false; } } return true; } LOptionsFile *ScribeWnd::GetOptions(bool Create) { if (!d->Options && Create) { LAssert(!"Not here... do it in LoadOptions."); return NULL; } return d->Options; } int OptionsFileCmp(OptionsInfo *a, OptionsInfo *b) { int64 Diff = b->Mod - a->Mod; return Diff < 0 ? -1 : (Diff > 0 ? 1 : 0); } OptionsInfo::OptionsInfo() { Score = 0; Mod = 0; Leaf = NULL; Usual = false; } OptionsInfo &OptionsInfo::operator =(char *p) { File = p; Leaf = LGetLeaf(File); if (Leaf) { char n[64]; sprintf_s(n, sizeof(n), "%s.xml", OptionsFileName); Usual = !_stricmp(n, Leaf); } return *this; } LAutoPtr OptionsInfo::Load() { // Read the file... size_t Count = 0; LXmlTag *Stores = NULL; LXmlTag *Acc = NULL; LAutoPtr Of(new LOptionsFile(File)); if (!Of) return Of; if (!Of->SerializeFile(false)) goto OnError; // Sanity check the options... Acc = Of->LockTag(OPT_Accounts, _FL); if (!Acc) goto OnError; Of->Unlock(); Stores = Of->LockTag(OPT_MailStores, _FL); if (!Stores) goto OnError; Count = Stores->Children.Length(); Of->Unlock(); if (Count == 0) goto OnError; return Of; OnError: Of.Reset(); return Of; } #define DEBUG_OPTS_SCAN 0 bool ScribeWnd::ScanForOptionsFiles(LArray &Files, LSystemPath PathType) { LString Root = LGetSystemPath(PathType); LDirectory Dir; char p[MAX_PATH_LEN]; if (IsUnitTest) Root += ".unittest"; #if DEBUG_OPTS_SCAN LgiTrace("%s:%i - Root='%s'\n", _FL, Root.Get()); #endif for (int b = Dir.First(Root); b; b = Dir.Next()) { if ( !Dir.IsDir() && Dir.Path(p, sizeof(p)) ) { LResolveShortcut(p, p, sizeof(p)); char *Ext = LGetExtension(Dir.GetName()); if (stristr(Dir.GetName(), OptionsFileName) != NULL && Ext && (!_stricmp(Ext, "xml") || !_stricmp(Ext, "bak"))) { OptionsInfo &i = Files.New(); i = p; i.Mod = Dir.GetLastWriteTime(); #if DEBUG_OPTS_SCAN LgiTrace("%s:%i - File='%s'\n", _FL, p); #endif } } } Files.Sort(OptionsFileCmp); // Scan through the results and pick out the normal file for (unsigned i=0; iOptions; i++) { if (Files[i].Usual) { d->Options = Files[i].Load(); #if DEBUG_OPTS_SCAN LgiTrace("%s:%i - Attempt '%s' = %p\n", _FL, Files[i].File.Get(), d->Options); #endif } } if (!d->Options) { // Scan through the alternative files and look for something // we can use. #if DEBUG_OPTS_SCAN LgiTrace("%s:%i - Scanning backups\n", _FL); #endif for (unsigned i=0; iOptions; i++) { if (!Files[i].Usual) { d->Options = Files[i].Load(); if (d->Options) { // Lets rename this baby back to the real filename LString Xml = OptionsFileName; Xml += ".xml"; LFile::Path Normal(Root, Xml); if (LFileExists(Normal)) FileDev->Delete(Normal); d->Options->SetFile(Normal); d->Options->SerializeFile(true); // sets to clean after changing filename. } } } } if (d->Options) { // Load OK: Clear out any old options files... #if DEBUG_OPTS_SCAN LgiTrace("%s:%i - Files.len=" LPrintfSizeT "\n", _FL, Files.Length()); #endif while (Files.Length() > 6) { auto Idx = Files.Length() - 1; auto &f = Files[Idx]; #if DEBUG_OPTS_SCAN LgiTrace("%s:%i - Delete '%s'\n", _FL, f.File.Get()); #endif FileDev->Delete(f.File); Files.DeleteAt(Idx); } } return d->Options != NULL; } bool ScribeWnd::IsUnitTest = false; bool ScribeWnd::LoadOptions() { bool Load = false; // Check if we are running unit tests... if ((IsUnitTest = LAppInst->GetOption("unittest"))) { d->UnitTestServer.Reset(new LUnitTestServer(this)); } // Look in the XGate folder #if WINNATIVE && !defined(_DEBUG) LRegKey xgate(false, "HKEY_CURRENT_USER\\Software\\XGate"); if (xgate.IsOk()) { char *Spool = xgate.GetStr("SpoolDir"); if (LDirExists(Spool)) { char File[MAX_PATH_LEN]; LMakePath(File, sizeof(File), Spool, "spool"); LMakePath(File, sizeof(File), File, OptionsFileName); strcat_s(File, sizeof(File), ".xml"); if (LFileExists(File)) { d->SetInstallMode(LOptionsFile::DesktopMode); LgiTrace("Selecting xgate mode based on options file path.\n"); d->Options.Reset(new LOptionsFile(File)); } } } #endif // Now look in the application install folder LArray Files; if (!d->Options && ScanForOptionsFiles(Files, LSP_APP_INSTALL)) { // File is in the install folder... d->SetInstallMode(LOptionsFile::PortableMode); LgiTrace("Selecting portable mode based on options file path.\n"); } // Look in the app root if ( !d->Options && ScanForOptionsFiles(Files, LSP_APP_ROOT) ) { // Desktop mode d->SetInstallMode(LOptionsFile::DesktopMode); LgiTrace("Selecting desktop mode based on options file path.\n"); } // Do multi-instance stuff if (d->Options && d->Options->GetFile()) { // Search for other instances of Scribe if (ScribeIpc) { int i; ScribeIpcInstance *InstLst = (ScribeIpcInstance*) ScribeIpc->GetPtr(); ScribeIpcInstance *Mul = 0; for (i=0; iMulPassword = Mul->Password; break; } } for (i=0; iIsScribe(), InstLst->Magic, InstLst->Pid); if (InstLst->IsScribe() && InstLst != ThisInst) { // LgiTrace("%s, %s\n", InstLst->OptionsPath, d->Options->GetFile()); if (Mul || _stricmp(InstLst->OptionsPath, d->Options->GetFile()) == 0) { int Pid = InstLst->Pid; OsAppArguments *Args = LAppInst->GetAppArgs(); if (!LIsProcess(Pid)) { continue; } char *Utf8 = 0; #if WINNATIVE Utf8 = WideToUtf8(Args->lpCmdLine); #else LStringPipe p; for (int i=1; iArgs; i++) { if (i > 1) p.Push(" "); char *Sp = strchr(Args->Arg[i], ' '); if (Sp) p.Push("\""); p.Push(Args->Arg[i]); if (Sp) p.Push("\""); } Utf8 = p.NewStr(); #endif if (Utf8) { size_t Len = strlen(Utf8); if (Len > 255) { InstLst->Flags |= SCRIBE_IPC_LONG_ARGS; InstLst->Flags &= ~SCRIBE_IPC_CONTINUE_ARGS; for (char *u = Utf8; Len > 0; u += 255) { ssize_t Part = MIN(sizeof(InstLst->Args)-1, Len); memcpy(InstLst->Args, u, Part); Len -= Part; int64 Start = LCurrentTime(); while (LCurrentTime() - Start < 60000) { if (TestFlag(InstLst->Flags, SCRIBE_IPC_CONTINUE_ARGS) && InstLst->Args[0] == 0) { Start = 0; break; } LSleep(10); } if (Start) { LgiTrace("%s:%i - SendLA timed out.\n", _FL); break; } } InstLst->Flags &= ~(SCRIBE_IPC_CONTINUE_ARGS | SCRIBE_IPC_LONG_ARGS); } else { strcpy_s(InstLst->Args, sizeof(InstLst->Args), Utf8); } DeleteArray(Utf8); ShutdownIpc(); LgiTrace("Passed args to the other running instance of Scribe (pid=%i)\n", Pid); LCloseApp(); return false; } else LgiTrace("%s:%i - No arguments to pass.\n", _FL); } } } if (Mul && LFileExists(Mul->OptionsPath)) { // No instance of Scribe is running, but MUL may be keeping // the previously run instance around. So we should run that // by using the options file and password from MUL's instance // record. d->Options.Reset(new LOptionsFile(Mul->OptionsPath)); } } // Insert ourselves into the instance list if (ThisInst) { strcpy_s(ThisInst->OptionsPath, sizeof(ThisInst->OptionsPath), d->Options->GetFile()); } } // Open file and load.. if (!Load && d->Options) { LOptionsFile *Opts = GetOptions(); Load = Opts->SerializeFile(false); if (Load) { LVariant v = d->GetInstallMode() == LOptionsFile::PortableMode; GetOptions()->SetValue(OPT_IsPortableInstall, v); } else { auto err = GetOptions()->GetError(); LgiMsg( this, LLoadString(IDS_ERROR_LR8_FAILURE), AppName, MB_OK, err); } } if (!d->Options) { // d->Options = new LOptionsFile(d->GetInstallMode(), OptionsFileName); return false; } if (d->Options) { LVariant v; if (!d->Options->GetValue(OPT_IsPortableInstall, v) && d->GetInstallMode() != LOptionsFile::UnknownMode) { v = d->GetInstallMode() == LOptionsFile::PortableMode; d->Options->SetValue(OPT_IsPortableInstall, v); } ScribeOptionsDefaults(d->Options); if (Load) { if (GetOptions()->GetValue(OPT_PrintSettings, v)) { auto *p = GetPrinter(); if (p) { LString s = v.Str(); p->Serialize(s, false); } } } if (d->Options->GetValue(OPT_PreviewLines, v)) { Mail::PreviewLines = v.CastInt32() != 0; } // upgrade smtp password const char *Pw = "SmtpPsw"; if (!GetOptions()->GetValue(OPT_EncryptedSmtpPassword, v)) { // no encrypted password, look for unencrypted password if (GetOptions()->GetValue(Pw, v)) { LPassword p; p.Set(v.Str()); p.Serialize(GetOptions(), OPT_EncryptedSmtpPassword, true); } } // if old un-encrypted password exists... // delete the key, we are now storing an encrypted // password if (GetOptions()->GetValue(Pw, v)) GetOptions()->DeleteValue(Pw); if (GetOptions()->GetValue(OPT_AdjustDateTz, v)) Mail::AdjustDateTz = !v.CastInt32(); if (!GetOptions()->GetValue(OPT_ConfirmDelete, v)) GetOptions()->SetValue(OPT_ConfirmDelete, v = true); if (!GetOptions()->GetValue(OPT_DelDirection, v)) GetOptions()->SetValue(OPT_DelDirection, v = DeleteActionPrev); if (GetOptions()->GetValue(OPT_SizeInKiB, v)) OptionSizeInKiB = v.CastInt32() != 0; if (GetOptions()->GetValue(OPT_RelativeDates, v)) ShowRelativeDates = v.CastInt32() != 0; // date format if (GetOptions()->GetValue(OPT_DateFormat, v)) { int Idx = v.CastInt32(); if (Idx >= 0 && Idx < CountOf(DateTimeFormats)) LDateTime::SetDefaultFormat(DateTimeFormats[Idx]); } // SSL debug logging if (GetOptions()->GetValue(OPT_DebugSSL, v)) SslSocket::DebugLogging = v.CastInt32() != 0; // Growl if (GetOptions()->GetValue(OPT_GrowlEnabled, v) && v.CastInt32()) { LVariant Ver, Bld; GetVariant(DomToStr(SdVersion), Ver); GetVariant("Build", Bld); LString n; n.Printf("%s\n%s", Ver.Str(), Bld.Str()); GrowlInfo("Scribe has started up...", n); } } #if LGI_EXCEPTIONS try { #endif // Default the font settings to the system font // if they don't already exist const char *OptFont[] = { OPT_EditorFont, OPT_PrintFont, OPT_HtmlFont, 0 }; int Index = 0; for (const char **Opt=OptFont; *Opt; Opt++, Index++) { LVariant v; if (!GetOptions()->GetValue(*Opt, v)) { LFontType Type; if (Type.GetSystemFont("System")) { if (Index == 2) { int Pt = Type.GetPointSize(); Type.SetPointSize(Pt+3); } Type.Serialize(GetOptions(), *Opt, true); } } } #if LGI_EXCEPTIONS } catch (...) { LgiMsg( this, LLoadString(IDS_ERROR_FONT_SETTINGS), AppName, MB_OK); } #endif return true; } bool ScribeWnd::SaveOptions() { LStringPipe Log(256); bool Status = false; bool WriteFailed = false; bool WndStateSet = false; RestartSave: if (d->Options && !d->Options->GetFile()) { bool PortableOk = true; char Path[MAX_PATH_LEN]; char Leaf[32]; sprintf_s(Leaf, sizeof(Leaf), "%s.xml", OptionsFileName); Log.Print("No current path for '%s', creating...\n", Leaf); LVariant v; GetOptions()->GetValue(OPT_IsPortableInstall, v); if (v.CastInt32()) { if (!LGetSystemPath(LSP_APP_INSTALL, Path, sizeof(Path))) { PortableOk = false; Log.Print("Error: LgiGetSystemPath(LSP_APP_INSTALL) failed.\n"); } else { LMakePath(Path, sizeof(Path), Path, Leaf); // Do write test to confirm we are good to go LFile f; if (f.Open(Path, O_WRITE)) { f.Close(); FileDev->Delete(Path, false); d->Options->SetFile(Path); } else { PortableOk = false; Log.Print("Warning: '%s' is not writable.\n", Path); } } } if (!v.CastInt32() || !PortableOk) { // Desktop mode then. if (v.CastInt32()) { const char *Msg = "Switching to desktop mode because the install folder is not writable."; Log.Print("%s\n", Msg); LgiMsg(this, Msg, AppName, MB_OK); GetOptions()->SetValue(OPT_IsPortableInstall, v = false); } if (!LGetSystemPath(LSP_APP_ROOT, Path, sizeof(Path))) { Log.Print("Error: LgiGetSystemPath(LSP_APP_ROOT) failed.\n"); } else { LMakePath(Path, sizeof(Path), Path, LAppInst->LBase::Name()); if (!LDirExists(Path)) { if (!FileDev->CreateFolder(Path)) { Log.Print("Error: CreateFolder('%s') failed.\n", Path); } } LMakePath(Path, sizeof(Path), Path, Leaf); // Do write test to confirm we are good to go LFile f; if (f.Open(Path, O_WRITE)) { f.Close(); FileDev->Delete(Path, false); d->Options->SetFile(Path); } else { Log.Print("Error: '%s' is not writable.\n", Path); } } } } if (d->Options && d->Options->GetFile() && d->Options->IsValid()) { // Backup options file char Backup[MAX_PATH_LEN]; strcpy_s(Backup, sizeof(Backup), d->Options->GetFile()); char *Ext = LGetExtension(Backup); if (Ext) { *--Ext = 0; LString s; for (int i=1; i<100; i++) { s.Printf("%s_%i.bak", Backup, i); if (!LFileExists(s)) break; } if (!LFileExists(s)) FileDev->Move(d->Options->GetFile(), s); } // Update some settings... #if LGI_VIEW_HANDLE if (Handle()) #endif WndStateSet = SerializeState(GetOptions(), OPT_ScribeWndPos, false); LVariant v; if (Splitter) GetOptions()->SetValue(OPT_SplitterPos, v = (int)Splitter->Value()); if (d->SubSplit) { auto First = d->SubSplit->GetViewAt(0); if (First == (LViewI*)SearchView) { auto Lst = (SearchView) ? d->SubSplit->GetViewAt(1) : NULL; if (Lst) GetOptions()->SetValue(OPT_SubSplitPos, v = (int)Lst->GetPos().Y()); } else GetOptions()->SetValue(OPT_SubSplitPos, v = (int)d->SubSplit->Value()); } // Write them... if (GetOptions()->SerializeFile(true)) { Status = true; } else { // We probably don't have write permissions to the install folder... Log.Print("Error: Options.Serialize failed.\n"); if (!WriteFailed) { // This blocks any possibility of an infinite loop WriteFailed = true; d->Options->SetFile(NULL); // Set desktop mode explicitly LVariant v; GetOptions()->GetValue(OPT_IsPortableInstall, v = false); Log.Print("Restarting save after setting desktop mode...\n"); goto RestartSave; } } } if (!Status) { LString a = Log.NewLStr(); LgiMsg(this, "Saving options failed:\n%s", AppName, MB_OK, a.Get()); } if (!WndStateSet) { LRect r(10, 10, 790, 590); SetPos(r); MoveToCenter(); } return Status; } // // Command Line Options: // // -m, -t : To recipient(s) // -f : The filename of the attachment // -b : Attach as a binary // -c : CC'd recipient(s) // -s : Subject for the email // -n : Send now... else UI is shown // -p : Print the file // -upgrade_folders : trigger a folder upgrade // -o : Load the following options file // -u : Load the following URL/file // void ScribeWnd::OnCommandLine() { // check command line args LString Str, File; bool CreateMail = false; CreateMail = LAppInst->GetOption("m", Str); if (!CreateMail) CreateMail = LAppInst->GetOption("t", Str); bool HasFile = LAppInst->GetOption("f", File); if (!CreateMail) CreateMail = HasFile; LString OpenArg; if (LAppInst->GetOption("u", OpenArg)) { LUri u(OpenArg); if (u.sProtocol) { OnUrl(OpenArg); } else if (LFileExists(OpenArg)) { LArray Files; Files.Add(OpenArg); OnReceiveFiles(Files); } } Mail *NewEmail = 0; if (CreateMail && Str) { // strip off quotes if needed char *In = Str, *Out = Str; for (; In && *In; In++) { if (!strchr("\'\"", *In)) { *Out++ = *In; } } *Out++ = 0; // create object NewEmail = dynamic_cast(CreateItem(MAGIC_MAIL, NULL, false)); if (NewEmail) { Mailto mt(this, Str); mt.Apply(NewEmail); // cc's? if (LAppInst->GetOption("c", Str)) { SetRecipients(this, Str, NewEmail->GetObject()->GetList(FIELD_TO), MAIL_ADDR_CC); } // attach a file? if (File) { if (LAppInst->GetOption("b")) { // attach as a binary file NewEmail->AttachFile(this, &File[0]); } else { // insert as the body LAutoString b(LReadTextFile(&File[0])); if (b) { NewEmail->SetBody(b); } } } // subject? if (LAppInst->GetOption("s", Str)) { NewEmail->SetSubject(Str); } // Send now or later? if (LAppInst->GetOption("n")) { // Check for exit after send option d->ExitAfterSend = LAppInst->GetOption("exit"); // now NewEmail->SetFlags(MAIL_CREATED | MAIL_READY_TO_SEND | NewEmail->GetFlags()); NewEmail->Save(); OnCommand(IDM_SEND_MAIL, 0, #ifndef __GTK_H__ Handle() #else NULL #endif ); } else { // later NewEmail->DoUI(); } } } // Pop3 on startup option LVariant n; if (GetOptions()->GetValue(OPT_Pop3OnStart, n) && n.CastInt32()) { OnCommand(IDM_RECEIVE_MAIL, 0, NULL); } } void ScribeWnd::SetCurrentIdentity(int i) { LVariant v = i; GetOptions()->SetValue(OPT_CurrentIdentity, v); if (DefaultIdentityItem) DefaultIdentityItem->Checked(i < 0); for (auto a: Accounts) { a->SetCheck(i == a->GetIndex()); } } ScribeAccount *ScribeWnd::GetCurrentAccount() { auto Idx = GetCurrentIdentity(); ScribeAccount *a = (Idx >= 0 && Idx < (ssize_t)Accounts.Length()) ? Accounts.ItemAt(Idx) : NULL; bool ValidId = a != NULL && a->IsValid(); if (!ValidId) { LAssert(!"No current identity?"); // Find a valid account to be the identity... for (auto a : Accounts) { if (!a->Send.Disabled() && a->Identity.IsValid()) { break; } } } return a; } int ScribeWnd::GetCurrentIdentity() { LVariant i; if (GetOptions()->GetValue(OPT_CurrentIdentity, i)) return i.CastInt32(); else if (ScribeState != ScribeInitializing) LgiTrace("%s:%i - No OPT_CurrentIdentity set.\n", _FL); return -1; } void ScribeWnd::SetupAccounts() { int i, CurrentIdentity = GetCurrentIdentity(); if (StatusPanel) { StatusPanel->Empty(); } #if !defined(COCOA) // FIXME LAssert(ReceiveMenu && PreviewMenu); #endif if (SendMenu) SendMenu->Empty(); if (ReceiveMenu) ReceiveMenu->Empty(); if (PreviewMenu) PreviewMenu->Empty(); if (IdentityMenu) { IdentityMenu->Empty(); } static bool Startup = true; bool ResetDefault = false; LArray Enabled; for (i=0; true; i++) { // char *s = 0; ScribeAccount *a = Startup ? new ScribeAccount(this, i) : Accounts[i]; if (a) { if (i == 0) { a->Create(); } a->Register(this); LVariant ReceiveName = a->Receive.Name(); LVariant ReceiveServer = a->Receive.Server(); LVariant SendServer = a->Send.Server(); if (i == 0 || ValidStr(ReceiveName.Str()) || ValidStr(ReceiveServer.Str()) || ValidStr(SendServer.Str()) ) { a->Send.SendItem = SendItem; a->Receive.ReceiveItem = ReceiveItem; a->Receive.PreviewItem = PreviewItem; if (!Accounts.HasItem(a)) { Accounts.Insert(a); } if (i) a->Create(); a->InitMenus(); // Identity Menu Item LVariant IdEmail = a->Identity.Email(); LVariant IdName = a->Identity.Name(); if (IdentityMenu && ValidStr(IdEmail.Str())) { char s[256]; if (IdName.Str()) sprintf_s(s, sizeof(s), "%s <%s>", IdName.Str(), IdEmail.Str()); else sprintf_s(s, sizeof(s), "<%s>", IdEmail.Str()); a->SetMenuItem(IdentityMenu->AppendItem(s, IDM_IDENTITY_BASE+i+1, !a->Send.Disabled())); if (a->Send.Disabled()) { a->SetCheck(false); if (i == CurrentIdentity) ResetDefault = true; } else { a->SetCheck(i == CurrentIdentity); Enabled[i] = a; } } } else { Accounts.Delete(a); DeleteObj(a); } } if (!a) break; } if ((ResetDefault || CurrentIdentity < 0) && Enabled.Length()) { for (unsigned i=0; iSetCheck(true); LVariant v; GetOptions()->SetValue(OPT_CurrentIdentity, v = (int)i); break; } } } Startup = false; if (ReceiveMenu && i == 0) { ReceiveMenu->AppendItem(LLoadString(IDS_NO_ITEMS), 0, false); } if (StatusPanel) { StatusPanel->OnAccountListChange(); } SetPulse(100); } ////////////////////////////////////////////////////////////////////////////// class LShutdown : public LDialog { LTextLabel *Msg; LButton *KillBtn; LButton *CancelBtn; bool Disconnected; public: ScribeAccount *Wait; List *Accounts; LShutdown(List *accounts) { Wait = 0; Disconnected = false; Accounts = accounts; LRect r( 0, 0, 320 + LAppInst->GetMetric(LGI_MET_DECOR_X), 70 + LAppInst->GetMetric(LGI_MET_DECOR_Y)); SetPos(r); MoveToCenter(); char Str[256]; sprintf_s(Str, sizeof(Str), "%s exiting...", AppName); LView::Name(Str); AddView(Msg = new LTextLabel(-1, 10, 10, 300, -1, "None")); AddView(KillBtn = new LButton(IDC_KILL, 70, 35, 60, 20, "Kill")); AddView(CancelBtn = new LButton(IDCANCEL, 140, 35, 60, 20, "Cancel")); if (KillBtn) { KillBtn->Enabled(false); } } void OnCreate() { SetPulse(100); } void OnPulse() { if (Accounts) { if (!Wait) { Wait = (*Accounts)[0]; if (Wait) { Disconnected = false; char s[256]; LVariant v = Wait->Receive.Name(); sprintf_s(s, sizeof(s), "Waiting for '%s'", v.Str() ? v.Str() : (char*)"Untitled..."); Msg->Name(s); Accounts->Delete(Wait); Wait->Stop(); KillBtn->Enabled(true); } else { SetPulse(); EndModal(true); } } if (Wait && !Wait->IsOnline()) { Wait = 0; Msg->Name("None"); KillBtn->Enabled(false); } } else { SetPulse(); EndModal(false); } } int OnNotify(LViewI *Ctrl, LNotification n) { switch (Ctrl->GetId()) { case IDC_KILL: { if (Wait) { if (!Disconnected) { Disconnected = true; Wait->Disconnect(); } else { Wait->Kill(); } } break; } case IDCANCEL: { EndModal(false); break; } } return 0; } }; bool ScribeWnd::OnRequestClose(bool OsShuttingDown) { if (FolderTasks.Length() > 0) { LgiTrace("%s:%i - %i folder tasks still busy...\n", _FL, FolderTasks.Length()); return false; } LString OnClose = LAppInst->GetConfig("Scribe.OnClose"); if (!d->IngoreOnClose && !OsShuttingDown && !Stricmp(OnClose.Get(), "minimize")) { SetZoom(LZoomMin); return false; } Visible(false); if (ScribeState != ScribeRunning) { // Inside a folder load/unload or initialization // Tell the loader to quit out... ScribeState = ScribeExiting; // Leave now, we can exit when we're ready return false; } else if (IsSending() || GetActiveThreads() > 0) { // whack up a shutdown window List Online; for (auto i: Accounts) { i->OnEndSession(); if (i->IsOnline()) { Online.Insert(i); } } auto Dlg = new LShutdown(&Online); Dlg->DoModal([this, Dlg](auto dlg, auto id) { if (id) { ScribeState = ScribeExiting; LCloseApp(); } else { ScribeState = ScribeRunning; Visible(true); } }); return false; // At the very minimum the app has to wait for the user to respond. } else { // End all sessions if any... for (auto i: Accounts) { i->OnEndSession(); } } // close all the other top level windows while (ThingUi::All.Length() > 0) { ThingUi *Ui = ThingUi::All.First(); if (!Ui->OnRequestClose(OsShuttingDown)) { ScribeState = ScribeRunning; Visible(true); return false; } size_t Start = ThingUi::All.Length(); Ui->Quit(); if (ThingUi::All.Length() >= Start) { LAssert(0); break; } } SerializeState(GetOptions(), OPT_ScribeWndPos, false); LCloseApp(); return LWindow::OnRequestClose(OsShuttingDown); } void ScribeWnd::DoOnTimer(LScriptCallback *c) { if (!c) return; auto Now = LCurrentTime(); if (c->PrevTs) { auto Since = Now - c->PrevTs; double Sec = (double)Since / 1000.0; if (Sec >= c->fParam) { // Call the function c->PrevTs = Now; LVirtualMachine Vm; LScriptArguments Args(&Vm); LVariant This((LDom*)this); Args.Add(&This); ExecuteScriptCallback(*c, Args); } } else { c->PrevTs = Now; } } void ScribeWnd::OnMinute() { if (Folders.Length() == 0) return; // Check for calendar event alarms... Calendar::CheckReminders(); // Check for any outgoing email that should be re-attempted... ScribeFolder *Outbox = GetFolder(FOLDER_OUTBOX); if (Outbox) { bool Resend = false; for (auto t : Outbox->Items) { Mail *m = t->IsMail(); if (m && !TestFlag(m->GetFlags(), MAIL_SENT) && TestFlag(m->GetFlags(), MAIL_READY_TO_SEND) && m->SendAttempts > 0) { Resend = true; break; } } if (Resend) Send(); } LArray Cb; if (GetScriptCallbacks(LOnTimer, Cb)) { for (auto c: Cb) { if (!c->Func) continue; if (c->fParam == 0.0) { // Work out the period from 'Data' char *s = c->Data.Str(); while (*s && IsWhiteSpace(*s)) s++; char *u = s; while (*u && !IsAlpha(*u)) u++; double v = atof(s); switch (*u) { case 's': case 'S': // seconds c->fParam = v; break; case 'm': case 'M': // mins c->fParam = v * LDateTime::MinuteLength; break; case 'h': case 'H': // hours c->fParam = v * LDateTime::HourLength; break; case 'd': case 'D': // days c->fParam = v * LDateTime::DayLength; break; default: { LgiTrace("%s:%i - Couldn't understand period '%s'\n", _FL, c->Data.Str()); c->Data.Empty(); break; } } if ((c->OnSecond = c->fParam < 60.0)) { d->OnSecondTimerCallbacks.Add(c); } } if (!c->OnSecond) DoOnTimer(c); } } } void ScribeWnd::OnHour() { // Force time zone update in case of daylight savings change. LDateTime::SystemTimeZone(true); // Check if we need should be doing a software update check static bool InSoftwareCheck = false; if (!InSoftwareCheck) { char s[64]; InSoftwareCheck = true; LVariant v; if (GetOptions()->GetValue(OPT_SoftwareUpdate, v) && v.CastInt32()) { LDateTime Now, Last; Now.SetFormat(GDTF_YEAR_MONTH_DAY); Last.SetFormat(Now.GetFormat()); Now.SetNow(); if (!GetOptions()->GetValue(OPT_SoftwareUpdateLast, v) || !Last.Set(v.Str())) { // Record now as the last check point Now.Get(s, sizeof(s)); GetOptions()->SetValue(OPT_SoftwareUpdateLast, v = s); } else if (GetOptions()->GetValue(OPT_SoftwareUpdateTime, v)) { // Valid last check date/time. switch (v.CastInt32()) { case 0: // Week Last.AddDays(7); break; case 1: // Month Last.AddMonths(1); break; case 2: // Year Last.AddMonths(12); break; default: LgiTrace("%s:%i - The option '%s' is not valid\n", _FL, OPT_SoftwareUpdateTime); return; } if (Last < Now) { // Save the last date for next time... Now.Get(s, sizeof(s)); GetOptions()->SetValue(OPT_SoftwareUpdateLast, v = s); // Check for update now... GetOptions()->GetValue(OPT_SoftwareUpdateIncBeta, v); IsSoftwareUpToDate( this, false, v.CastInt32() != 0, [this](auto s, auto Info) { if (s == SwOutOfDate) if (UpgradeSoftware(Info, this, true)) LCloseApp(); }); } } } InSoftwareCheck = false; } } bool ScribeWnd::SaveDirtyObjects(int TimeLimitMs) { bool Status = false; if (Thing::DirtyThings.Length() > 0) { static bool SavingObjects = false; if (!SavingObjects) { SavingObjects = true; LArray WriteTimes; // ssize_t StartDirty = Thing::DirtyThings.Length(); uint64 Start = LCurrentTime(); for (unsigned i=0; iSave(NULL)) { WriteTimes.Add((int) (LCurrentTime() - WriteStart)); LAssert(!ThingType::DirtyThings.HasItem(t)); Status = true; } else { LgiTrace("Failed to save thing type 0x%x\n", t->Type()); FailedWrites++; if (FailedWrites > 2) { while (ThingType::DirtyThings.Length()) ThingType::DirtyThings[0]->SetDirty(false); FailedWrites = 0; } } } } SavingObjects = false; /* if (Status) { LStringPipe p; p.Print("WriteTimes: "); for (unsigned i=0; iLastTs >= 1000) { d->LastTs = Now; OnPulseSecond(); } } } void ScribeWnd::OnPulseSecond() { #if PROFILE_ON_PULSE LProfile Prof("NewMailLst handling"); Prof.HideResultsIfBelow(50); #endif if (Mail::NewMailLst.Length() > 0) { LVariant Blink; if (GetOptions()->GetValue(OPT_BlinkNewMail, Blink) && Blink.CastInt32()) { TrayIcon.Value((TrayIcon.Value() == TRAY_ICON_MAIL) ? TRAY_ICON_NONE : TRAY_ICON_MAIL); } } else { bool Err = false; for (auto a: Accounts) { if (!a->Receive.GetStatus() || !a->Send.GetStatus()) { Err = true; } } TrayIcon.Value(Err ? TRAY_ICON_ERROR : TRAY_ICON_NORMAL); } #if PROFILE_ON_PULSE Prof.Add("StatusPanel handling"); #endif if (StatusPanel) { StatusPanel->OnPulse(); } #if PROFILE_ON_PULSE Prof.Add("OnXXXX handling"); #endif LDateTime Now; Now.SetNow(); if (d->LastMinute != Now.Minutes()) // Check every minute... { d->LastMinute = Now.Minutes(); OnMinute(); } if (d->LastHour != Now.Hours()) // Check every hour... { d->LastHour = Now.Hours(); OnHour(); } { // These timers need to be checked every second... for (auto c: d->OnSecondTimerCallbacks) DoOnTimer(c); } #if PROFILE_ON_PULSE Prof.Add("Instance handling"); #endif if (ThisInst && ValidStr(ThisInst->Args)) { LStringPipe p; p.Push(ThisInst->Args); if (ThisInst->Flags & SCRIBE_IPC_LONG_ARGS) { ThisInst->Flags |= SCRIBE_IPC_CONTINUE_ARGS; int64 Start = LCurrentTime(); while ( TestFlag(ThisInst->Flags, SCRIBE_IPC_LONG_ARGS) && LCurrentTime() - Start < 60000) { ZeroObj(ThisInst->Args); while ( TestFlag(ThisInst->Flags, SCRIBE_IPC_LONG_ARGS) && !ThisInst->Args[0] && LCurrentTime() - Start < 60000) { LSleep(10); } p.Push(ThisInst->Args); } } ZeroObj(ThisInst->Args); LAutoString Arg(p.NewStr()); if (Arg) { OsAppArguments AppArgs(0, 0); LgiTrace("Received cmd line: %s\n", Arg.Get()); AppArgs.Set(Arg); LAppInst->SetAppArgs(AppArgs); if (LAppInst->GetOption("m") && LAppInst->GetOption("f")) ; else LAppInst->OnCommandLine(); OnCommandLine(); if (GetZoom() == LZoomMin) SetZoom(LZoomNormal); Visible(true); } } #if PROFILE_ON_PULSE Prof.Add("PreviewPanel handling"); #endif if (PreviewPanel) { PreviewPanel->OnPulse(); } } void ScribeWnd::AddFolderToMru(char *FileName) { if (FileName) { // read MRU List Files; int i; for (i=0; i<10; i++) { char Key[32]; LVariant f; sprintf_s(Key, sizeof(Key), "FolderMru.%i", i); if (GetOptions()->GetValue(Key, f)) { Files.Insert(NewStr(f.Str())); GetOptions()->DeleteValue(Key); } } // remove FileName if present for (auto f: Files) { if (_stricmp(f, FileName) == 0) { Files.Delete(f); DeleteArray(f); break; } } // insert FileName at the start of the list Files.Insert(NewStr(FileName)); // write MRU for (i=0; i<10; i++) { char *n = Files.ItemAt(i); if (n) { char Key[32]; sprintf_s(Key, sizeof(Key), "FolderMru.%i", i); LVariant f; GetOptions()->SetValue(Key, f = n); } else break; } // Clean up Files.DeleteArrays(); } } bool ScribeWnd::CleanFolders(ScribeFolder *f) { if (!f) return false; if (f->Select()) { f->SerializeFieldWidths(); } for (ScribeFolder *c = f->GetChildFolder(); c; c = c->GetNextFolder()) { CleanFolders(c); } return true; } void ScribeWnd::OnFolderChanged(LDataFolderI *folder) { } bool ScribeWnd::OnFolderTask(LEventTargetI *Ptr, bool Add) { if (Add) { if (FolderTasks.HasItem(Ptr)) { LAssert(!"Can't add task twice."); return false; } FolderTasks.Add(Ptr); return true; } else { if (!FolderTasks.HasItem(Ptr)) { LAssert(!"Item not part of task list."); return false; } FolderTasks.Delete(Ptr); return true; } } LMailStore *ScribeWnd::GetDefaultMailStore() { LMailStore *Def = 0; for (unsigned i=0; i Def->Priority()) { Def = &Folders[i]; } } } } return Def; } bool HasMailStore(LXmlTag *MailStores, char *Name) { for (auto t : MailStores->Children) { char *StoreName = t->GetAttr(OPT_MailStoreName); if (StoreName && Name && !_stricmp(StoreName, Name)) return true; } return false; } LDataStoreI *ScribeWnd::CreateDataStore(const char *_Full, bool CreateIfMissing) { LString Full(_Full); auto Ext = LGetExtension(Full); if (Ext) { if (!_stricmp(Ext, "mail2")) { LgiMsg(this, LLoadString(IDS_MAIL2_DEPRECATED), AppName, MB_OK, Full.Get()); } else if (!_stricmp(Ext, "mail3")) { return OpenMail3(Full, this, CreateIfMissing); } else if (!_stricmp(Ext, "sqlite")) { LTrimDir(Full); return OpenMail3(Full, this, CreateIfMissing); } else { LgiTrace("%s:%i - Not a valid mail store extension: %s\n", _FL, Full.Get()); LAssert(!"Not a valid mail store extension."); } } else LgiTrace("%s:%i - No extension for CreateDataStore: %s\n", _FL, Full.Get()); return NULL; } class MailStoreUpgrade : public LProgressDlg, public LDataPropI { public: ScribeWnd *App = NULL; LDataStoreI *Ds = NULL; int Status = -1; LString Error; MailStoreUpgrade(ScribeWnd *app, LDataStoreI *ds) : LProgressDlg(app, -1) { SetParent(App = app); Ds = ds; SetCanCancel(false); SetDescription("Upgrading mail store..."); SetPulse(1000); Ds->Upgrade(this, this, [this](auto status) { Status = status; }); } ~MailStoreUpgrade() { } void OnPulse() override { if (Status >= 0) { EndModal(0); return; } return LProgressDlg::OnPulse(); } LDataPropI &operator =(LDataPropI &p) { LAssert(0); return *this; } Store3Status SetStr(int id, const char *str) override { switch (id) { case Store3UiError: Error = str; break; default: LAssert(!"Impl me."); return Store3Error; break; } return Store3Success; } }; bool ScribeWnd::ProcessFolder(LDataStoreI *&Store, int StoreIdx, char *StoreName) { if (Store->GetInt(FIELD_VERSION) == 0) { // version error LgiMsg(this, LLoadString(IDS_ERROR_FOLDERS_VERSION), AppName, MB_OK, 0, Store->GetInt(FIELD_VERSION)); return false; } if (Store->GetInt(FIELD_READONLY)) { LgiMsg(this, LLoadString(IDS_ERROR_READONLY_FOLDERS), AppName); } // get root item LDataFolderI *Root = Store->GetRoot(); if (!Root) return false; ScribeFolder *&Mailbox = Folders[StoreIdx].Root; Mailbox = new ScribeFolder; if (Mailbox) { Mailbox->App = this; Mailbox->SetObject(Root, false, _FL); Root->SetStr(FIELD_FOLDER_NAME, StoreName); Root->SetInt(FIELD_FOLDER_TYPE, MAGIC_NONE); } #ifdef TEST_OBJECT_SIZE // debug/repair code if (Root->StoreSize != Root->Sizeof()) { SizeErrors[0]++; Root->StoreSize = Root->Sizeof(); if (Root->Object) { Root->Object->StoreDirty = true; } } #endif // Insert the root object and then... Tree->Insert(Mailbox); // Recursively load the rest of the tree { // LProfile p("Loadfolders"); Mailbox->LoadFolders(); } // This forces a re-pour to re-order the folders according to their // sort settings. Tree->UpdateAllItems(); if (ScribeState != ScribeExiting) { // Show the tree Mailbox->Expanded(Folders[StoreIdx].Expanded); // Checks the folders for a number of required objects // and creates them if required auto StoreType = Store->GetInt(FIELD_STORE_TYPE); if (StoreType == Store3Sqlite) Validate(&Folders[StoreIdx]); else if (StoreType < 0) LAssert(!"Make sure you impl the FIELD_STORE_TYPE field in the store."); // FIXME // AddFolderToMru(Full); } return true; } struct LoadMailStoreState : public LView::ViewEventTarget { typedef std::function BoolCb; typedef std::function IntCb; ScribeWnd *App; BoolCb Callback; // Final cb for entire LoadMailStoreState execution LOptionsFile *Options = NULL; LXmlTag *MailStores = NULL; LArray Que; int StoreIdx = 0; bool OptionsDirty = false; bool Status = false; std::function ReturnWithEvent; std::function ReturnOnDialog; std::function AskStoreUpgrade; LoadMailStoreState(ScribeWnd *app, std::function callback) : ViewEventTarget(app, M_LOAD_NEXT_MAIL_STORE), App(app), Callback(callback) { Options = App->GetOptions(); ReturnWithEvent = [this](bool s) { PostEvent(M_LOAD_NEXT_MAIL_STORE); return s; }; ReturnOnDialog = [this](bool s) { return s; }; AskStoreUpgrade = [this](auto FolderPath, auto Details, auto cb) { auto result = LgiMsg(App, LLoadString(IDS_MAILSTORE_UPGRADE_Q), AppName, MB_YESNO, FolderPath, ValidStr(Details) ? Details : "n/a"); cb(result); }; MailStores = Options->LockTag(OPT_MailStores, _FL); // Load up some work in the queue and start the iteration... if (MailStores) Que = MailStores->Children; } void Start() { PostEvent(M_LOAD_NEXT_MAIL_STORE); } bool OnStatus(bool b) { if (MailStores) Options->Unlock(); if (OptionsDirty) App->SaveOptions(); if (Callback) Callback(b); delete this; return b; } LMessage::Result OnEvent(LMessage *Msg) override { if (Msg->Msg() == M_LOAD_NEXT_MAIL_STORE) { if (!MailStores) OnStatus(false); else Iterate(); } return 0; } // This will process one item off the Que, // Or if no work is available call PostIterate to // finish the process. // // In most cases this function should complete with // ReturnWithEvent to trigger the next iteration... bool Iterate() { // No more work, so do the completion step: if (Que.Length() == 0) return PostIterate(); // There are 2 exits modes for this function: // // 1) Normal exit where we should post a M_LOAD_NEXT_MAIL_STORE to ourselves to // start the next iteration by calling ReturnWithEvent. // // 2) A dialog was launched and we should NOT post a M_LOAD_NEXT_MAIL_STORE event // by calling ReturnOnDialog. The dialog's callback will do that later. // Get the next Xml tag off the queue: auto MailStore = Que[0]; Que.DeleteAt(0, true); if (!MailStore->IsTag(OPT_MailStore)) return ReturnWithEvent(false); // Read the folders.. auto Path = MailStore->GetAttr(OPT_MailStoreLocation); auto ContactUrl = MailStore->GetAttr(OPT_MailStoreContactUrl); auto CalUrl = MailStore->GetAttr(OPT_MailStoreCalendarUrl); if (!Path && !ContactUrl && !CalUrl) { LgiTrace("%s:%i - No mail store path (%i).\n", _FL, StoreIdx); return ReturnWithEvent(false); } // If disabled, skip: if (MailStore->GetAsInt(OPT_MailStoreDisable) > 0) return ReturnWithEvent(false); // Check and validate the folder name: auto StoreName = MailStore->GetAttr(OPT_MailStoreName); if (!StoreName) { char Tmp[256]; for (int i=1; true; i++) { sprintf_s(Tmp, sizeof(Tmp), "Folders%i", i); if (!HasMailStore(MailStores, Tmp)) break; } MailStore->SetAttr(OPT_MailStoreName, Tmp); StoreName = MailStore->GetAttr(OPT_MailStoreName); OptionsDirty = true; } // Build a LMailStore entry in App->Folders: auto &Folder = App->Folders[StoreIdx]; Folder.Name = StoreName; if (Path) { // Mail3 folders on disk... LFile::Path p; if (LIsRelativePath(Path)) { p = Options->GetFile(); p--; p += Path; } else p = Path; auto Full = p.GetFull(); LVariant CreateFoldersIfMissing; Options->GetValue(OPT_CreateFoldersIfMissing, CreateFoldersIfMissing); // Sniff type... auto Ext = LGetExtension(Full); if (!Ext) return ReturnWithEvent(false); if (!Folder.Store) Folder.Store = App->CreateDataStore(Full, CreateFoldersIfMissing.CastInt32() != 0); if (!Folder.Store) { LgiTrace("%s:%i - Failed to create data store for '%s'\n", _FL, Full.Get()); return ReturnWithEvent(false); } Folder.Path = Full; } else if (ContactUrl || CalUrl) { // Remove Webdav folders... Folder.Store = new WebdavStore(App, App, StoreName); } else { return ReturnWithEvent(false); } LDataStoreI *&Store = Folder.Store; auto ex = MailStore->GetAsInt(OPT_MailStoreExpanded); if (ex >= 0) Folder.Expanded = ex != 0; // Check if the mail store requires upgrading... auto MsState = (Store3Status)Store->GetInt(FIELD_STATUS); if (MsState == Store3UpgradeRequired) { LgiTrace("%s:%i - this=%p\n", _FL, this); auto Details = Store->GetStr(FIELD_STATUS); AskStoreUpgrade(Folder.Path.Get(), ValidStr(Details) ? Details : "n/a", [this, Store, MailStore](auto result) { if (result == IDYES) { auto Prog = new MailStoreUpgrade(App, Store); Prog->DoModal([this, MailStore](auto dlg, auto code) { // Upgrade complete, finish the iteration.. Iterate2(MailStore); delete dlg; }); return; } // Upgrade not allowed, so do next iteration... ReturnWithEvent(false); }); return ReturnOnDialog(true); } else if (MsState == Store3Error) { auto ErrMsg = Store->GetStr(FIELD_ERROR); auto a = new LAlert(App, AppName, ErrMsg ? ErrMsg : LLoadString(IDS_ERROR_FOLDERS_STATUS), LLoadString(IDS_EDIT_MAIL_STORES), LLoadString(IDS_OK)); a->DoModal([this](auto dlg, auto code) { delete dlg; if (code == 1) { App->PostEvent(M_COMMAND, IDM_MANAGE_MAIL_STORES); // This fails the whole LoadMailStores process, because the user // is going to edit the mail store list via the dialog; OnStatus(false); } else { // We can't load this mail store, so do the next iteration... ReturnWithEvent(true); } }); return ReturnOnDialog(false); } // No upgrade or error, just keep going. return Iterate2(MailStore); } // This also should complete by calling ReturnWithEvent/ReturnOnDialog. bool Iterate2(LXmlTag *MailStore) { auto &Folder = App->Folders[StoreIdx]; auto StoreName = MailStore->GetAttr(OPT_MailStoreName); // check password LString FolderPsw; if ((FolderPsw = Folder.Store->GetStr(FIELD_STORE_PASSWORD))) { bool Verified = false; if (ValidStr(App->d->MulPassword)) { Verified = App->d->MulPassword.Equals(FolderPsw, false); App->d->MulPassword.Empty(); } if (!Verified) { auto Dlg = new LInput(App, "", LLoadString(IDS_ASK_FOLDER_PASS), AppName, true); Dlg->DoModal([this, Dlg, FolderPsw, StoreName](auto dlg, auto id) { auto psw = Dlg->GetStr(); delete dlg; if (id == IDOK) { auto &Folder = App->Folders[StoreIdx]; if (psw == FolderPsw) { Status |= App->ProcessFolder(Folder.Store, StoreIdx, StoreName); } else { // Clear the folder and don't increment the StoreIdx... Folder.Empty(); App->Folders.PopLast(); ReturnWithEvent(false); return; } } Iterate3(); }); return ReturnOnDialog(true); } } else { Status |= App->ProcessFolder(Folder.Store, StoreIdx, StoreName); } return Iterate3(); } bool Iterate3() { StoreIdx++; return ReturnWithEvent(true); } bool PostIterate() { if (Status) { // Force load some folders... ScribeFolder *Folder = App->GetFolder(FOLDER_CALENDAR); if (Folder) Folder->LoadThings(); Folder = App->GetFolder(FOLDER_FILTERS); if (Folder) Folder->LoadThings(); for (auto ms: App->Folders) { if (!ms.Root) continue; for (auto c = ms.Root->GetChildFolder(); c; c = c->GetNextFolder()) { if (c->GetItemType() == MAGIC_CONTACT || c->GetItemType() == MAGIC_FILTER) c->LoadThings(); } } List c; App->GetContacts(c); // Set selected folder to Inbox by default // if the user hasn't selected a folder already if (App->ScribeState != ScribeWnd::ScribeExiting && App->Tree && !App->Tree->Selection()) { LVariant StartInFolder; Options->GetValue(OPT_StartInFolder, StartInFolder); ScribeFolder *Start = NULL; if (ValidStr(StartInFolder.Str())) { Start = App->GetFolder(StartInFolder.Str()); } if (!Start) { Start = App->GetFolder(FOLDER_INBOX); } if (Start && App->Tree) { App->Tree->Select(Start); } } } Options->DeleteValue(OPT_CreateFoldersIfMissing); // Set system folders ScribeFolder *f = App->GetFolder(FOLDER_INBOX); if (f) f->SetSystemFolderType(Store3SystemInbox); f = App->GetFolder(FOLDER_OUTBOX); if (f) f->SetSystemFolderType(Store3SystemOutbox); f = App->GetFolder(FOLDER_SENT); if (f) f->SetSystemFolderType(Store3SystemSent); f = App->GetFolder(FOLDER_SPAM); if (f) f->SetSystemFolderType(Store3SystemSpam); return OnStatus(Status); } }; void ScribeWnd::LoadMailStores(std::function Callback) { if (auto s = new LoadMailStoreState(this, Callback)) s->Start(); } void ScribeWnd::LoadFolders(std::function Callback) { AppState PrevState = ScribeState; ScribeState = ScribeLoadingFolders; // Setup Mailstores tag { LXmlTag *MailStores = GetOptions()->LockTag(OPT_MailStores, _FL); if (!MailStores) { // Check if we can upgrade the old folder tag char n[32]; sprintf_s(n, sizeof(n), "%s-Folders", LGetOsName()); LVariant OldFolders; GetOptions()->GetValue(n, OldFolders); // Create mail store element.. GetOptions()->CreateTag(OPT_MailStores); if ((MailStores = GetOptions()->LockTag(OPT_MailStores, _FL))) { if (OldFolders.Str()) { LXmlTag *Store = MailStores->CreateTag(OPT_MailStore); if (Store) { char Opts[MAX_PATH_LEN]; LMakePath(Opts, sizeof(Opts), GetOptions()->GetFile(), ".."); auto Rel = LMakeRelativePath(Opts, OldFolders.Str()); Store->SetAttr(OPT_MailStoreLocation, Rel ? Rel.Get() : OldFolders.Str()); // No need to ask the user for a store name, it'll be // asked later in this method anyway... // Leave the old folder tag in the xml in case the user // downgrades to v1.xx } } } } GetOptions()->Unlock(); if (!MailStores) { if (Callback) Callback(false); return; } } // Set loading flags CmdSend.Enabled(false); CmdReceive.Enabled(false); CmdPreview.Enabled(false); LoadMailStores([this, PrevState, Callback](auto Status) { if (Tree) { for (auto a: Accounts) { if (!a->Receive.Disabled() && a->Receive.IsPersistant()) a->Receive.Connect(0, false); } } using BoolFn = std::function; auto FinishLoad = new BoolFn ( [this, PrevState, Callback](bool Status) { if (ScribeState == ScribeExiting) { LCloseApp(); } else { d->FoldersLoaded = true; PostEvent(M_SCRIBE_LOADED); } if (ScribeState == ScribeExiting) LCloseApp(); ScribeState = PrevState; if (Callback) Callback(Status); } ); if (Folders.Length() == 0) { auto Dlg = new ScribeFolderDlg(this); Dlg->DoModal([this, Dlg, FinishLoad, Callback, &Status](auto dlg, auto id) { if (id == IDOK) { bool CreateMailStore = false; if (Dlg->Create) { // create folders if (LFileExists(Dlg->FolderFile)) { if (LgiMsg(this, LLoadString(IDS_ERROR_FOLDERS_ALREADY_EXIST), AppName, MB_YESNO) == IDYES) CreateMailStore = true; else LgiMsg(this, LLoadString(IDS_ERROR_WONT_OVERWRITE_FOLDERS), AppName); } else if ((Status = CreateFolders(Dlg->FolderFile))) CreateMailStore = true; } else CreateMailStore = true; if (CreateMailStore) { LXmlTag *MailStores = GetOptions()->LockTag(OPT_MailStores, _FL); if (MailStores) { LXmlTag *Store = MailStores->CreateTag(OPT_MailStore); if (Store) { char p[MAX_PATH_LEN]; LMakePath(p, sizeof(p), GetOptions()->GetFile(), ".."); auto RelPath = LMakeRelativePath(p, Dlg->FolderFile); Store->SetAttr(OPT_MailStoreLocation, RelPath ? RelPath.Get() : Dlg->FolderFile.Get()); } GetOptions()->Unlock(); LoadMailStores(NULL); } } } if (id) (*FinishLoad)(Status); else if (Callback) Callback(false); delete FinishLoad; delete dlg; }); } else { (*FinishLoad)(Status); delete FinishLoad; } }); } bool ScribeWnd::UnLoadFolders() { if (FolderTasks.Length() > 0 || ScribeState == ScribeLoadingFolders) { // Um, we can't unload folders right now // something is already happening... return false; } AppState PrevState = ScribeState; ScribeState = ScribeUnloadingFolders; OnSelect(); if (MailList) { ScribeFolder *Container = MailList->GetContainer(); if (Container) { // save folder settings Container->SerializeFieldWidths(); } MailList->SetContainer(NULL); MailList->RemoveAll(); } int Error = 0; while (Thing::DirtyThings.Length() > 0) { if (!SaveDirtyObjects()) { Error++; LgiTrace("%s:%i - SaveDirtyObjects failed.\n", _FL); if (Error > 100) { // I think we're stuck... return false; } } } // Unload IMAP folders... for (auto a: Accounts) { if (!a->Receive.Disabled() && a->Receive.IsPersistant()) { a->Receive.Disconnect(); } } if (GetOptions()) { // Unload local folders... LXmlTag *MailStores = GetOptions()->LockTag(OPT_MailStores, _FL); for (size_t i=0; iExpanded(); for (auto ms: MailStores->Children) { char *StoreName = ms->GetAttr(OPT_MailStoreName); if (Folders[i].Name.Equals(StoreName)) { ms->SetAttr(OPT_MailStoreExpanded, Expanded); break; } } } DeleteObj(Folders[i].Root); DeleteObj(Folders[i].Store); } if (MailStores) { GetOptions()->Unlock(); MailStores = NULL; } } Folders.Length(0); d->FoldersLoaded = false; if (ScribeState == ScribeExiting) LCloseApp(); ScribeState = PrevState; return true; } void ScribeWnd::BuildDynMenus() { if (MailMenu) { LString SendMail = LLoadString(IDS_SEND_MAIL); LString ReceiveMail = LLoadString(IDS_RECEIVE_MAIL); LString PreviewMail = LLoadString(IDS_PREVIEW_ON_SERVER); auto ReceiveAll = LLoadString(IDS_RECEIVE_ALL_ACCOUNTS); if (!CmdReceive.MenuItem && ReceiveAll) CmdReceive.MenuItem = MailMenu->AppendItem(ReceiveAll, IDM_RECEIVE_ALL, true); if (!SendMenu && SendMail) { auto s = SendMail.SplitDelimit("\t"); SendMenu = MailMenu->AppendSub(s[0]); } if (!ReceiveMenu && ReceiveMail) { auto s = ReceiveMail.SplitDelimit("\t"); ReceiveMenu = MailMenu->AppendSub(s[0]); } if (!PreviewMenu && PreviewMail) { auto s = PreviewMail.SplitDelimit("\t"); PreviewMenu = MailMenu->AppendSub(s[0]); } } if (!NewTemplateMenu) { auto i = Menu->FindItem(IDM_NO_TEMPLATES); if (i) { NewTemplateMenu = i->GetParent(); } } if (NewTemplateMenu) { NewTemplateMenu->Empty(); int d = 0; ScribeFolder *Templates = GetFolder(FOLDER_TEMPLATES, NULL, true); if (Templates) { Templates->LoadThings(); for (auto i: Templates->Items) { Mail *m = i->IsMail(); if (m) { NewTemplateMenu->AppendItem(m->GetSubject() ? m->GetSubject() : (char*)"(no subject)", IDM_NEW_FROM_TEMPLATE + d, true); d++; } } if (d == 0) { NewTemplateMenu->AppendItem(LLoadString(IDS_NO_ITEMS_IN_FOLDER), -1, false); } } else { NewTemplateMenu->AppendItem(LLoadString(IDS_NO_TEMPLATES), -1, false); } } } int ScribeWnd::GetToolbarHeight() { return (Commands) ? MAX(Commands->Y()-1, 20) : 20; } LToolBar *ScribeWnd::LoadToolbar(LViewI *Parent, const char *File, LAutoPtr &Img) { if (!Img) Img.Reset(LLoadImageList(File)); if (!Img) { LAssert(!"Missing image resource."); return NULL; } LToolBar *Tools = NULL; if (Img) { Tools = new LToolBar; if (Tools) Tools->SetImageList(Img, Img->TileX(), Img->TileY(), false); } else { Tools = LgiLoadToolbar(Parent, File); } if (Tools) { LVariant SizeAdj; LFont *f = Tools->GetFont(); if (f) { if (GetOptions()->GetValue(OPT_UiFontSize, SizeAdj)) { SizeAdj.Cast(GV_INT32); SizeAdj.Value.Int -= 2; f->PointSize(f->PointSize()+SizeAdj.Value.Int); } } Tools->GetCss(true)->BorderSpacing(LCss::Len(LCss::LenPx, SCRIBE_TOOLBAR_BORDER_SPACING_PX)); Tools->TextLabels(ShowToolbarText()); } return Tools; } void ScribeWnd::SetListPane(LView *v) { ThingList *ThingLst = dynamic_cast(v); DynamicHtml *Html = dynamic_cast(v); if (!ThingLst) { DeleteObj(SearchView); if (MailList) MailList->RemoveAll(); } v->Sunken(SUNKEN_CTRL); if ((MailList = ThingLst)) { DeleteObj(TitlePage); if (GetCtrlValue(IDM_ITEM_FILTER)) { OnCommand(IDM_ITEM_FILTER, 0, NULL); } } else { TitlePage = Html; } SetLayout(); } bool ScribeWnd::SetItemPreview(LView *v) { v->Sunken(SUNKEN_CTRL); if (d->SubSplit->IsAttached()) { if (d->LastLayout == 2) { Splitter->SetViewAt(1, v); } else { d->SubSplit->SetViewAt(1, v); } return true; } return false; } ScribeWnd::LayoutMode ScribeWnd::GetEffectiveLayoutMode() { LVariant Mode; GetOptions()->GetValue(OPT_LayoutMode, Mode); ScribeFolder *Cur = GetCurrentFolder(); if (Cur && Cur->IsRoot()) { Mode = FoldersAndList; } if (Mode.CastInt32() == 0) { Mode = FoldersListAndPreview; } return (LayoutMode) Mode.CastInt32(); } void ScribeWnd::SetLayout(LayoutMode Mode) { // int TreeWidth = Tree ? Tree->X() : 200; // int PreviewHt = PreviewPanel ? PreviewPanel->Y() : 200; if (Mode > 0) { LVariant v; GetOptions()->SetValue(OPT_LayoutMode, v = (int)Mode); } Mode = GetEffectiveLayoutMode(); if (!Splitter) return; bool JustPreviewPane = (Mode == FoldersAndList && d->LastLayout == FoldersListAndPreview) || (Mode == FoldersListAndPreview && d->LastLayout == FoldersAndList); LView *Content = NULL; if (TitlePage) Content = TitlePage; else if (MailList) Content = MailList; if (JustPreviewPane) { // Optimized path for hide/show the preview pane that doesn't destroy the tree // control and cause it to lose focus... otherwise we can't set it's focus due // to some weird windows interaction. switch (Mode) { default: case FoldersListAndPreview: { if (Content) Content->Sunken(SUNKEN_CTRL); if (PreviewPanel) PreviewPanel->Sunken(SUNKEN_CTRL); Splitter->SetViewAt(1, d->SubSplit); d->SubSplit->SetVertical(true); int Idx = 0; if (SearchView) d->SubSplit->SetViewAt(Idx++, SearchView); d->SubSplit->SetViewAt(Idx++, Content); d->SubSplit->SetViewAt(Idx++, PreviewPanel); break; } case FoldersAndList: { if (Content) Content->Sunken(SUNKEN_CTRL); if (SearchView) { #if LGI_VIEW_HANDLE if (!d->SubSplit->Handle()) Splitter->SetViewAt(1, d->SubSplit); #endif d->SubSplit->SetVertical(true); Splitter->SetViewAt(1, d->SubSplit); int Idx = 0; if (SearchView) d->SubSplit->SetViewAt(Idx++, SearchView); d->SubSplit->SetViewAt(Idx++, Content); } else { d->SubSplit->Detach(); Splitter->SetViewAt(1, Content); } break; } } } else { if (Tree) Tree->Sunken(SUNKEN_CTRL); if (Content) Content->Sunken(SUNKEN_CTRL); if (PreviewPanel) PreviewPanel->Sunken(SUNKEN_CTRL); switch (Mode) { default: case FoldersListAndPreview: { Splitter->SetVertical(false); d->SubSplit->SetVertical(true); Splitter->SetViewAt(0, Tree); Splitter->SetViewAt(1, d->SubSplit); int Idx = 0; if (SearchView) d->SubSplit->SetViewAt(Idx++, SearchView); d->SubSplit->SetViewAt(Idx++, Content); d->SubSplit->SetViewAt(Idx++, PreviewPanel); DeleteObj(d->SearchSplit); break; } case PreviewOnBottom: { Splitter->SetVertical(true); d->SubSplit->SetVertical(false); Splitter->SetViewAt(0, d->SubSplit); Splitter->SetViewAt(1, PreviewPanel); d->SubSplit->SetViewAt(0, Tree); if (SearchView) { if (!d->SearchSplit) d->SearchSplit = new LBox; d->SubSplit->SetViewAt(1, d->SearchSplit); d->SearchSplit->SetVertical(true); d->SearchSplit->SetViewAt(0, SearchView); d->SearchSplit->SetViewAt(1, Content); } else { d->SubSplit->SetViewAt(1, Content); DeleteObj(d->SearchSplit); } break; } case FoldersAndList: { Splitter->SetVertical(false); Splitter->SetViewAt(0, Tree); if (SearchView) { d->SubSplit->SetVertical(true); Splitter->SetViewAt(1, d->SubSplit); d->SubSplit->SetViewAt(0, SearchView); d->SubSplit->SetViewAt(1, Content); } else { d->SubSplit->Detach(); Splitter->SetViewAt(1, Content); } DeleteObj(d->SearchSplit); break; } case ThreeColumn: { Splitter->SetVertical(false); Splitter->SetViewAt(0, Tree); if (SearchView) { d->SubSplit->SetVertical(true); Splitter->SetViewAt(1, d->SubSplit); d->SubSplit->SetViewAt(0, SearchView); d->SubSplit->SetViewAt(1, Content); } else { d->SubSplit->Detach(); Splitter->SetViewAt(1, Content); } Splitter->SetViewAt(2, PreviewPanel); DeleteObj(d->SearchSplit); break; } } } if (!SearchView) { LVariant Pos = 200; GetOptions()->GetValue(OPT_SplitterPos, Pos); if (Pos.CastInt32() < 10) Pos = 200; Splitter->Value(Pos.CastInt32()); LRect r = Splitter->GetPos(); r.x2++; Splitter->SetPos(r); if (d->SubSplit->IsAttached()) { Pos = 200; GetOptions()->GetValue(OPT_SubSplitPos, Pos); if (Pos.CastInt32() < 10) Pos = 200; d->SubSplit->Value(Pos.CastInt32()); } } PourAll(); #ifdef LINUX LYield(); #endif d->LastLayout = Mode; } void ScribeWnd::SetupUi() { // Show the window if (!SerializeState(GetOptions(), OPT_ScribeWndPos, true)) { LRect r(0, 0, 1023, 767); SetPos(r); MoveToCenter(); } Visible(true); // Main toolbar Commands = LoadToolbar(this, GetResourceFile(ResToolbarFile), ToolbarImgs); if (Commands) { Commands->Attach(this); #ifdef MAC Commands->Raised(false); #else Commands->Raised(RAISED_LOOK); #endif Commands->AppendButton(RemoveAmp(LLoadString(IDS_NEW_EMAIL)), IDM_NEW_EMAIL, TBT_PUSH, true, IMG_NEW_MAIL); Commands->AppendButton(RemoveAmp(LLoadString(IDS_NEW_CONTACT)), IDM_NEW_CONTACT, TBT_PUSH, true, IMG_NEW_CONTACT); Commands->AppendSeparator(); CmdSend.ToolButton = Commands->AppendButton(RemoveAmp(LLoadString(IDS_SEND)), IDM_SEND_MAIL, TBT_PUSH, true, IMG_SEND); Commands->AppendButton("+", IDM_RECEIVE_AND_SEND, TBT_PUSH, true, -2); CmdReceive.ToolButton = Commands->AppendButton(RemoveAmp(LLoadString(IDS_RECEIVE)), IDM_RECEIVE_MAIL, TBT_PUSH, true, IMG_RECEIVE); CmdPreview.ToolButton = Commands->AppendButton(RemoveAmp(LLoadString(IDS_PREVIEW)), IDM_PREVIEW_POP3, TBT_PUSH, true, IMG_PREVIEW); Commands->AppendSeparator(); Commands->AppendButton(RemoveAmp(LLoadString(IDS_DELETE)), IDM_DELETE, TBT_PUSH, true, IMG_TRASH); Commands->AppendButton(RemoveAmp(LLoadString(IDS_SPAM)), IDM_DELETE_AS_SPAM, TBT_PUSH, true, IMG_DELETE_SPAM); Commands->AppendSeparator(); Commands->AppendButton(RemoveAmp(LLoadString(IDS_REPLY)), IDM_REPLY, TBT_PUSH, true, IMG_REPLY); Commands->AppendButton(RemoveAmp(LLoadString(IDS_REPLYALL)), IDM_REPLY_ALL, TBT_PUSH, true, IMG_REPLY_ALL); Commands->AppendButton(RemoveAmp(LLoadString(IDS_FORWARD)), IDM_FORWARD, TBT_PUSH, true, IMG_FORWARD); Commands->AppendButton(RemoveAmp(LLoadString(IDS_BOUNCE)), IDM_BOUNCE, TBT_PUSH, true, IMG_BOUNCE); Commands->AppendSeparator(); Commands->AppendButton(RemoveAmp(LLoadString(IDS_PRINT)), IDM_PRINT, TBT_PUSH, true, IMG_PRINT); Commands->AppendButton(RemoveAmp(LLoadString(IDS_CALENDAR)), IDM_CALENDAR, TBT_PUSH, true, IMG_CALENDAR); Commands->AppendButton(RemoveAmp(LLoadString(IDS_ITEM_FILTER)), IDM_ITEM_FILTER, TBT_TOGGLE, true, IMG_SEARCH); Commands->AppendButton(RemoveAmp(LLoadString(IDS_THREAD)), IDM_THREAD, TBT_TOGGLE, true, IMG_THREADS); d->ShowConsoleBtn = Commands->AppendButton(RemoveAmp(LLoadString(IDS_SHOW_CONSOLE)), IDM_SHOW_CONSOLE, TBT_PUSH, true, IMG_CONSOLE_NOMSG); Commands->AppendSeparator(); Commands->AppendButton(RemoveAmp(LLoadString(IDS_HELP)), IDM_HELP, TBT_PUSH, true, IMG_HELP); Commands->Customizable(GetOptions(), OPT_ScribeWndToolbar); if (d->ScriptToolbar.Reset(new LScriptUi(Commands))) d->ScriptToolbar->SetupCallbacks(this, 0, 0, LApplicationToolbar); PourAll(); } CmdSend.Enabled(false); CmdReceive.Enabled(false); // Preview and status windows PreviewPanel = new LPreviewPanel(this); StatusPanel = new AccountStatusPanel(this, ImageList); if (PreviewPanel && StatusPanel) { #ifdef MAC StatusPanel->Raised(false); #else StatusPanel->Raised(RAISED_LOOK); #endif StatusPanel->Attach(this); PourAll(); } // Splitter window, for folders and item list Tree = new MailTree(this); if (ImageList) { Tree->AskImage(true); Tree->SetImageList(ImageList, false); } d->SubSplit = new LBox; Splitter = new LBox; if (Splitter) { Splitter->Attach(this); #ifdef MAC Splitter->Raised(false); #else Splitter->Raised(RAISED_LOOK); #endif SetLayout(); } #if WINNATIVE TrayIcon.Load(MAKEINTRESOURCE(IDI_SMALL)); TrayIcon.Load(MAKEINTRESOURCE(IDI_ERR)); TrayIcon.Load(MAKEINTRESOURCE(IDI_MAIL)); TrayIcon.Load(MAKEINTRESOURCE(IDI_BLANK)); #else TrayIcon.Load(_T("tray_small.png")); TrayIcon.Load(_T("tray_error.png")); TrayIcon.Load(_T("tray_mail.png")); #endif LStringPipe s(256); LVariant UserName; s.Print("%s", AppName); if (GetOptions()->GetValue(OPT_UserName, UserName)) { s.Print(" [%s]", UserName.Str()); } auto AppTitle = s.NewLStr(); TrayIcon.Name(AppTitle); Name(AppTitle); TrayIcon.Value(TRAY_ICON_NORMAL); TrayIcon.Visible(true); auto Item = Menu->FindItem(IDM_SCRIPTING_CONSOLE); if (Item) { LVariant v; if (GetOptions()->GetValue(OPT_ShowScriptConsole, v)) { Item->Checked(v.CastInt32() != 0); LScribeScript::Inst->ShowScriptingWindow(Item->Checked()); } } if (Tree) { Tree->Focus(true); } } Thing *ScribeWnd::CreateItem(int Type, ScribeFolder *Folder, bool Ui) { auto FolderStore = Folder && Folder->GetObject() ? Folder->GetObject()->GetStore() : NULL; auto DefaultStore = GetDefaultMailStore(); auto Store = FolderStore ? FolderStore : (DefaultStore ? DefaultStore->Store : NULL); if (!Store) { LAssert(!"no store"); LgiTrace("%s:%i - No store for creating calendar object.\n", _FL); return NULL; } auto Obj = Store->Create(Type); if (!Obj) { LAssert(!"create failed"); LgiTrace("%s:%i - store failed to create object.\n", _FL); return NULL; } #define HANDLE_CREATE_ITEM(Magic, Type) \ case Magic: \ { \ auto o = new Type(this, Obj); \ if (!o) \ { \ LgiTrace("%s:%i - Alloc failed.\n", _FL); \ break; \ } \ if (Folder) o->SetParentFolder(Folder); \ if (Ui) o->DoUI(); \ return o; \ } switch ((uint32_t)Type) { case MAGIC_MAIL: { // create a new mail message Mail *m = new Mail(this, Obj); if (!m) { LgiTrace("%s:%i - Alloc failed.\n", _FL); break; } if (!m->GetObject()) { m->DecRef(); return 0; } m->OnCreate(); if (Folder) m->SetParentFolder(Folder); if (Ui) m->DoUI(); return m; } HANDLE_CREATE_ITEM(MAGIC_CONTACT, Contact) HANDLE_CREATE_ITEM(MAGIC_CALENDAR, Calendar) HANDLE_CREATE_ITEM(MAGIC_FILTER, Filter) HANDLE_CREATE_ITEM(MAGIC_GROUP, ContactGroup) default: LAssert(!"Unhandled object type."); break; } return NULL; } void ScribeWnd::OnPaint(LSurface *pDC) { #if 0 pDC->Colour(LColour::Red); pDC->Rectangle(); #else LCssTools Tools(this); auto c = GetClient(); // c.Offset(0, -26); Tools.PaintContent(pDC, c); #endif } int CompareContacts(Contact **a, Contact **b) { if (a && b) { auto A = (*a)->GetFirst(); auto B = (*b)->GetFirst(); if (A && B) return _stricmp(A, B); } return 0; } bool ScribeWnd::OpenAMail(ScribeFolder *Folder) { if (Folder && Tree) { Folder->LoadThings(); for (auto i: Folder->Items) { Mail *m = i->IsMail(); if (m && !(m->GetFlags() & MAIL_READ)) { Tree->Select(Folder); m->DoUI(); return true; } } for (auto *t=Folder->GetChild(); t; t=t->GetNext()) { ScribeFolder *f = dynamic_cast(t); if (OpenAMail(f)) { return true; } } } return false; } void AddContactToMenu(LSubMenu *Menu, Contact *c, ssize_t Index) { if (!c || Index < 0) return; auto Email = c->GetEmail(); if (!Email) return; // has an email, list it auto First = c->GetFirst(); auto Last = c->GetLast(); if (First || Last) { char Buf[256]; sprintf_s(Buf, sizeof(Buf), "%s %s", (First)?First:"", (Last)?Last:""); auto Item = Menu->AppendItem(Buf, TRAY_CONTACT_BASE + (int)Index, true); if (Item) Item->Icon(ICON_CONTACT); } } void ScribeWnd::AddContactsToMenu(LSubMenu *Menu) { if (!Menu) return; d->TrayMenuContacts.Sort(CompareContacts); if (((ssize_t)d->TrayMenuContacts.Length() << 4) > GdcD->Y() - 200) { // Group contacts by starting letter LArray Alpha[26]; LArray Other; for (auto c: d->TrayMenuContacts) { auto First = c->GetFirst(); auto Last = c->GetLast(); auto Email = c->GetEmail(); if (Email) { // has an email, list it if (First || Last) { auto Name = First?First:Last; if ( (*Name >= 'a' && *Name <= 'z') || (*Name >= 'A' && *Name <= 'Z') ) { int Ind = tolower(*Name) - 'a'; if (Ind >= 0 && Ind < CountOf(Alpha)) Alpha[Ind].Add(c); else Other.Add(c); } else Other.Add(c); } } } for (int i=0; i 0) { char Group[64]; sprintf_s(Group, sizeof(Group), "%c...", 'a' + i); auto Sub = Menu->AppendSub(Group); if (Sub) { for (auto c: Alpha[i]) AddContactToMenu(Sub, c, d->TrayMenuContacts.IndexOf(c)); } } } if (Other.Length()) { auto Sub = Menu->AppendSub("Other..."); if (Sub) { for (auto c: Other) AddContactToMenu(Sub, c, d->TrayMenuContacts.IndexOf(c)); } } } else { // Display all... for (size_t i=0; iTrayMenuContacts.Length(); i++) { AddContactToMenu(Menu, d->TrayMenuContacts[i], i); } } } void ScribeWnd::OnUrl(const char *Url) { LUri u(Url); if (u.sProtocol && !_stricmp(u.sProtocol, "mailto")) { CreateMail(0, Url, 0); } } void ScribeWnd::OnReceiveFiles(LArray &Files) { int UnknownFormats = 0; int64 Period = LCurrentTime() - LastDrop; if (Period > 500) // Lock out drops within 500ms of an LGI drop { LString sSend, sPages; bool HasSend = LAppInst->GetOption("send", sSend); bool HasPrint = LAppInst->GetOption("p", sPages); LArray NewMail; for (unsigned i=0; i str(new LFile); if (str->Open(f, O_READ)) { if (t->Import(t->AutoCast(str), MimeType)) { if (HasSend) { Mail *m = t->IsMail(); if (m) { NewMail.Add(m); m->SetFlags(m->GetFlags() | MAIL_CREATED | MAIL_READ); } } else { t->DoUI(); } } } } } else UnknownFormats++; } bool SendNow = sSend ? atoi(sSend) != 0 : false; for (unsigned i=0; iSetFlags(m->GetFlags() | MAIL_READY_TO_SEND); m->Save(); } else if (HasPrint) { ThingPrint(NULL, m, GetPrinter(), 0, sPages ? atoi(sPages) : 0); } else { m->DoUI(); } } if (SendNow) Send(); } } void ScribeWnd::OnTrayClick(LMouse &m) { if (m.Down()) { #ifndef MAC // No support for different mouse button info so the default // action is to show the sub-menu of contacts and actions. if (m.IsContextMenu()) #endif { LWindow::OnTrayClick(m); } #ifndef MAC else if (m.Left()) { if (m.Double()) { if (Mail::NewMailLst.Length() > 0) { ScribeFolder *InBox = GetFolder(FOLDER_INBOX); OpenAMail(InBox); } } else { if (GetZoom() == LZoomMin) { SetZoom(LZoomNormal); } else { if (Obscured()) SetZoom(LZoomNormal); // Bounce in front, first. else SetZoom(LZoomMin); } Visible(true); MoveOnScreen(); Raise(); if (MailList) { MailList->Focus(true); } } } else if (m.Middle()) { Mail::NewMailLst.Empty(); } #endif } } void ScribeWnd::OnTrayMenu(LSubMenu &m) { m.SetImageList(ImageList, false); d->TrayMenuContacts.Length(0); #ifdef MAC m.AppendItem(LLoadString(IDS_OPEN), IDM_OPEN); m.AppendSeparator(); #endif LHashTbl,Contact*> Added; LArray Srcs = GetThingSources(MAGIC_CONTACT); for (auto c: Srcs) { c->LoadThings(); for (auto i: c->Items) { Contact *c = i->IsContact(); if (!c) continue; bool IsAdded = false; auto Emails = c->GetEmails(); for (auto e: Emails) { if (Added.Find(e)) IsAdded = true; } if (Emails.Length() && !IsAdded) { for (auto e: Emails) Added.Add(e, c); d->TrayMenuContacts.Add(c); } } } AddContactsToMenu(&m); m.AppendSeparator(); if (Mail::NewMailLst.Length() > 0) { int i=0; for (auto ml: Mail::NewMailLst) { LStringPipe p; // This code figures out how many UTF characters to print. We // can't split a UTF character because downstream conversions // will fail. const char *Subj = ml->GetSubject(); if (!Subj) Subj = "(No Subject)"; LUtf8Ptr u((uint8_t*)Subj); while ((int32)u && u.GetPtr() - (uchar*)Subj < 64) u++; ssize_t Bytes = u.GetPtr() - (uchar*)Subj; p.Print("%.*s, %s <%s>", Bytes, Subj?Subj:(char*)"(No Subject)", ml->GetFromStr(FIELD_NAME), ml->GetFromStr(FIELD_EMAIL)); LAutoString a(p.NewStr()); LAssert(LIsUtf8(a)); auto Item = m.AppendItem(a, TRAY_MAIL_BASE+i++, true); if (Item) { Item->Icon(ICON_UNREAD_MAIL); } } m.AppendSeparator(); } if (GetZoom() == LZoomMin) { m.AppendItem(LLoadString(IDS_OPEN), IDM_OPEN, true); } auto NewMail = m.AppendItem(LLoadString(IDS_NEW_EMAIL), IDM_NEW_EMAIL); if (NewMail) NewMail->Icon(ICON_UNSENT_MAIL); m.AppendItem(LLoadString(IDS_EXIT), IDM_EXIT); } void ScribeWnd::OnTrayMenuResult(int MenuId) { switch (MenuId) { case IDM_OPEN: { if (GetZoom() == LZoomMin) { SetZoom(LZoomNormal); } Visible(true); Raise(); if (MailList) { MailList->Focus(true); } break; } case IDM_NEW_EMAIL: { CreateMail(); break; } case IDM_EXIT: { d->IngoreOnClose = true; PostEvent(M_CLOSE); break; } default: { auto i = MenuId - TRAY_CONTACT_BASE; Contact *c = d->TrayMenuContacts.IdxCheck(i) ? d->TrayMenuContacts[i] : NULL; if (c) { CreateMail(c); } Mail *m = Mail::NewMailLst[MenuId - TRAY_MAIL_BASE]; if (m) { Mail::NewMailLst.Delete(m); m->DoUI(); } break; } } } void ScribeWnd::OnZoom(LWindowZoom Action) { if (Action == LZoomMin) { LVariant i; if (GetOptions()->GetValue(OPT_MinimizeToTray, i) && i.CastInt32()) { Visible(false); } } } struct UserInput { std::function Callback; LView *Parent; LString Msg; bool Password; UserInput() { Password = false; } }; void ScribeWnd::GetUserInput(LView *Parent, LString Msg, bool Password, std::function Callback) { if (InThread()) { auto Inp = new LInput(Parent ? Parent : this, "", Msg, AppName, Password); Inp->DoModal([this, Inp, Callback](auto dlg, auto id) { if (Callback) Callback(id ? Inp->GetStr() : NULL); delete dlg; }); } auto i = new UserInput; i->Parent = Parent; i->Msg = Msg; i->Password = Password; i->Callback = Callback; if (!PostEvent(M_GET_USER_INPUT, (LMessage::Param)i)) { LAssert(!"PostEvent failed."); if (Callback) Callback(NULL); } } LMessage::Result ScribeWnd::OnEvent(LMessage *Msg) { TrayIcon.OnEvent(Msg); BayesianFilter::OnEvent(Msg); switch (Msg->Msg()) { case M_UNIT_TEST: { LAutoPtr j((LJson*)Msg->A()); if (!j) break; auto cmd = j->Get("cmd"); if (!cmd) break; if (cmd.Equals("somecmd")) { } break; } case M_CALENDAR_SOURCE_EVENT: { CalendarSource *cs = (CalendarSource*)Msg->A(); LAutoPtr m((LMessage*)Msg->B()); if (cs && m) cs->OnEvent(m); break; } case M_GET_USER_INPUT: { LAutoPtr i((UserInput*)Msg->A()); LAssert(i); LAssert(InThread()); // Least we get stuck in an infinite loop GetUserInput(i->Parent, i->Msg, i->Password, i->Callback); break; } case M_SET_HTML: { LAutoPtr Html((LString*)Msg->A()); if (PreviewPanel && Html) { LVariant Ret; LArray Arg; Arg.Add(new LVariant(Html->Get())); PreviewPanel->CallMethod(DomToStr(SdSetHtml), &Ret, Arg); Arg.DeleteObjects(); } break; } case M_NEW_CONSOLE_MSG: { if (d->ShowConsoleBtn) d->ShowConsoleBtn->Image(IDM_CONSOLE_MSGS); break; } case M_NEEDS_CAP: { LAutoString c((char*)Msg->A()); LAutoString param((char*)Msg->B()); NeedsCapability(c, param); return 0; break; } case M_STORAGE_EVENT: { LDataStoreI *Store = LDataStoreI::Map.Find((int)Msg->A()); if (Store) Store->OnEvent((void*)Msg->B()); break; } case M_SCRIBE_SET_MSG_FLAG: { LAutoString p((char*)Msg->A()); if (p) { char *d = strrchr(p, '/'); if (d) { *d++ = 0; ScribeFolder *f = GetFolder(p); if (f) { LUri u; LString a = u.DecodeStr(d); f->GetMessageById(a, [this, NewFlag=(int)Msg->B()](auto r) { if (r) { int ExistingFlags = r->GetFlags(); r->SetFlags(ExistingFlags | NewFlag); } }); } } } break; } case M_SCRIBE_DEL_THING: { Thing *t = (Thing*)Msg->A(); DeleteObj(t); break; } case M_SCRIBE_LOADED: { if (d->FoldersLoaded) { // Ok let the user in CmdSend.Enabled(true); CmdReceive.Enabled(true); CmdPreview.Enabled(true); } break; } case M_SCRIBE_THREAD_DONE: { // Finialize connection AccountThread *Thread = (AccountThread*) Msg->A(); Accountlet *Acc = (Accountlet*) Msg->B(); if (Thread && Acc) { OnAfterConnect(Acc->GetAccount(), Acc->IsReceive()); d->NewMailTimeout = 2; Acc->OnThreadDone(); } else { LAssert(0); } break; } case M_SCRIBE_MSG: { char *m = (char*)Msg->A(); if (m) { if (Msg->B()) { if (LgiMsg(this, m, AppName, MB_YESNO) == IDYES) { PostEvent(M_COMMAND, IDM_OPTIONS, 0); } } else { LgiMsg(this, "%s", AppName, MB_OK, m); } DeleteArray(m); } break; } /* case M_SCRIBE_LOG_MSG: { List *Log = (List*)Msg->A(); LAutoPtr Entry((LogEntry*)Msg->B()); if (ScribeState != ScribeExiting && Log && Entry) { Log->Insert(Entry.Release()); // Trim long list... while (Log->Length() > 1024) { LogEntry *e = Log->First(); Log->Delete(e); DeleteObj(e); } } break; } */ case M_SCRIBE_NEW_MAIL: { if (Lock(_FL)) { if (NewMailDlg) { NewMailDlg->AddThings(&Mail::NewMailLst); } Unlock(); } break; } case M_SCRIBE_OPEN_THING: { Thing *t = (Thing*) Msg->A(); if (t) { t->DoUI(); } break; } case M_SCRIBE_ITEM_SELECT: { if (!MailList || !IsAttached()) break; List Things; MailList->GetSelection(Things); OnSelect(&Things); break; } case M_URL: { LAutoPtr Url((LString*)Msg->A()); if (Url) { LUri u(*Url); if (u.sProtocol && !_stricmp(u.sProtocol, "mailto")) { Mailto mt(this, *Url); if (mt.To.Length() > 0) { Thing *t = CreateItem(MAGIC_MAIL, NULL, false); if (t) { Mail *m = t->IsMail(); if (m) { mt.Apply(m); m->DoUI(); } else DeleteObj(t); } } } } break; } } return LWindow::OnEvent(Msg); } bool ScribeWnd::IsMyEmail(const char *Email) { if (Email) { LVariant e; for (auto a : *GetAccounts()) { LVariant e = a->Identity.Email(); if (e.Str() && _stricmp(Email, e.Str()) == 0) { return true; } } } return false; } int ScribeWnd::GetMaxPages() { return d->PrintMaxPages; } void ScribeWnd::ThingPrint(std::function Callback, ThingType *m, LPrinter *Printer, LView *Parent, int MaxPages) { d->PrintMaxPages = MaxPages; if (!Printer) Printer = GetPrinter(); if (!Printer) { if (Callback) Callback(false); return; } Thing *t = dynamic_cast(m); if (!t) { if (Callback) Callback(false); return; } auto Events = new ScribePrintContext(this, t); Printer->Print( Events, [this, Events, Parent, Printer, Callback](auto pages) { if (pages == Events->OnBeginPrintError) { LgiMsg(Parent, "Printing failed: %s", AppName, MB_OK, Printer->GetErrorMsg().Get()); if (Callback) Callback(false); } else if (Callback) { Callback(true); } delete Events; }, AppName, -1, Parent ? Parent : this); } bool ScribeWnd::MailReplyTo(Mail *m, bool All) { bool Status = false; if (m) { LDataStoreI *Store = m->GetObject() ? m->GetObject()->GetStore() : NULL; LDataI *NewMailObj = Store && Store->GetInt(FIELD_STORE_TYPE) == Store3Sqlite ? Store->Create(MAGIC_MAIL) : NULL; Mail *NewMail = new Mail(this, NewMailObj); if (NewMail) { if (NewMail->GetObject()) { NewMail->OnReply(m, All, true); LView *w = NewMail->DoUI(); if (w) { LViewI *t = w->FindControl(IDC_TEXT); if (t) { t->Focus(true); } } Status = true; } else DeleteObj(NewMail); } } return Status; } bool ScribeWnd::MailForward(Mail *m) { bool Status = false; if (m) { Mail *NewMail = new Mail(this); if (NewMail) { if (NewMail->OnForward(m, true)) { NewMail->DoUI(); Status = true; } else { NewMail->DecRef(); } } } return Status; } bool ScribeWnd::MailBounce(Mail *m) { bool Status = false; if (m) { Mail *NewMail = new Mail(this); if (NewMail) { if (NewMail->OnBounce(m, true)) { NewMail->DoUI(); Status = true; } else { DeleteObj(NewMail); } } } return Status; } Mail *ScribeWnd::CreateMail(Contact *c, const char *Email, const char *Name) { Mail *m = dynamic_cast(CreateItem(MAGIC_MAIL, NULL, false)); if (m) { bool IsMailTo = false; if (Email) { IsMailTo = _strnicmp(Email, "mailto:", 7) == 0; if (IsMailTo) { Mailto mt(this, Email); mt.Apply(m); } } MailUi *UI = dynamic_cast(m->DoUI()); if (UI) { if (c) { UI->AddRecipient(c); } if (Email && !IsMailTo) { UI->AddRecipient(Email, Name); } } } return m; } Mail *ScribeWnd::LookupMailRef(const char *MsgRef, bool TraceAllUids) { if (!MsgRef) return 0; LAutoString p(NewStr(MsgRef)); char *RawUid = strrchr(p, '/'); if (RawUid) { *RawUid++ = 0; LUri u; LString Uid = u.DecodeStr(RawUid); // Try the mail message map first... Mail *m = Mail::GetMailFromId(Uid); if (m) return m; // Ok, not found, so look in last known folder... ScribeFolder *f = GetFolder(p); if (f) { for (auto t : f->Items) { Mail *m = t->IsMail(); if (m) { auto s = m->GetMessageId(); if (s && !strcmp(s, Uid)) { return m; } if (TraceAllUids) LgiTrace("\t%s\n", s); } } } } return 0; } void ScribeWnd::OnBayesAnalyse(const char *Msg, const char *WhiteListEmail) { LString s, q; s.Printf("
%s
", Msg); if (WhiteListEmail) { q.Printf(LLoadString(IDS_REMOVE_WHITELIST), WhiteListEmail); s += LString("
") + q; } s += ""; LHtmlMsg([this, WhiteListEmail=LString(WhiteListEmail)](auto result) { if (result == IDYES) RemoveFromWhitelist(WhiteListEmail); }, this, s, AppName, WhiteListEmail ? MB_YESNO : MB_OK); } bool ScribeWnd::OnBayesResult(const char *MailRef, double Rating) { Mail *m = LookupMailRef(MailRef); if (m) return OnBayesResult(m, Rating); #ifdef _DEBUG else { LgiTrace("%s:%i - error finding mail ref: %s\n", _FL, MailRef); LookupMailRef(MailRef, true); LAssert(!"We should always be able to resolve the reference, unless m is completely deleted"); } #endif return false; } bool ScribeWnd::OnBayesResult(Mail *m, double Rating) { if (!m) return false; LVariant v; GetOptions()->GetValue(OPT_BayesThreshold, v); double BayesThresh = v.CastDouble(); if (BayesThresh < 0.1) BayesThresh = 0.1; if (BayesThresh > 1.0) BayesThresh = 1.0; if (Rating < BayesThresh) { // Not spam, so we continue new mail processing if (m->NewEmail == Mail::NewEmailBayes) { List Nm; Nm.Insert(m); m->NewEmail = Mail::NewEmailGrowl; OnNewMail(&Nm, true); } return false; } // Spam is pink! m->SetMarkColour(Rgb32(255, 0, 0)); m->SetFlags(m->GetFlags() | MAIL_BAYES_SPAM); ScribeBayesianFilterMode FilterMode = BayesOff; if (GetOptions()->GetValue(OPT_BayesFilterMode, v)) FilterMode = (ScribeBayesianFilterMode)v.CastInt32(); if (FilterMode == BayesTrain) { // Move to folder LVariant MoveToPath; if (!GetOptions()->GetValue(OPT_BayesMoveTo, MoveToPath)) { MoveToPath = "/Spam/Probably"; } ScribeFolder *f = GetFolder(MoveToPath.Str()); if (f) { LArray Items; Items.Add(m); f->MoveTo(Items, false, [this, m](auto result, auto status) { List obj; obj.Insert(m); OnNewMail(&obj, false); }); } } else { m->DeleteAsSpam(this); } return true; } static int AccountCmp(ScribeAccount *a, ScribeAccount *b, int Data) { return a->Identity.Sort() - b->Identity.Sort(); } class ScribePasteState : public LProgressDlg { ScribeWnd *App = NULL; ScribeFolder *Folder = NULL; LAutoPtr Data; ssize_t Size = 0; LDataStoreI::StoreTrans Trans; LProgressPane *LoadPane = NULL, *SavePane = NULL; ScribeClipboardFmt *tl = NULL; uint32_t Errors = 0; ssize_t Idx = 0; enum PasteState { LoadingThings, SavingThings, } State = LoadingThings; public: ScribePasteState(ScribeWnd *app, ScribeFolder *folder, LAutoPtr data, ssize_t size) : LProgressDlg(app), App(app), Folder(folder), Data(data), Size(size) { // Paste 'ScribeThingList' tl = (ScribeClipboardFmt*)Data.Get(); Trans = Folder->GetObject()->GetStore()->StartTransaction(); LoadPane = ItemAt(0); LoadPane->SetDescription("Loading objects..."); LoadPane->SetRange(tl->Length()); SavePane = Push(); SavePane->SetRange(tl->Length()); SavePane->SetDescription("Saving: No errors..."); // LProgressDlg will do a SetPulse in it's OnCreate } void OnPulse() { auto Start = LCurrentTime(); static int TimeSlice = 300; //ms if (State == LoadingThings) { while ( Idx < tl->Length() && !IsCancelled() && LCurrentTime() - Start < TimeSlice) { Thing *t = tl->ThingAt(Idx++); if (!t) continue; auto Obj = t->GetObject(); if (Obj->GetInt(FIELD_LOADED) < Store3Loaded) Obj->SetInt(FIELD_LOADED, Store3Loaded); } Value(Idx); if (Idx >= tl->Length()) { State = SavingThings; Idx = 0; } } else if (State == SavingThings) { while ( Idx < tl->Length() && !IsCancelled() && LCurrentTime() - Start < TimeSlice) { Thing *t = tl->ThingAt(Idx++); if (!t) continue; auto Obj = t->GetObject(); LAssert(Obj->GetInt(FIELD_LOADED) == Store3Loaded); // Load loop should have done this already Thing *Dst = App->CreateItem(Obj->Type(), Folder, false); if (Dst) { *Dst = *t; Dst->Update(); if (!Dst->Save(Folder)) { LString s; s.Printf("Saving: " LPrintfSSizeT " error(s)", ++Errors); SetDescription(s); } } else Errors++; } SavePane->Value(Idx); if (Idx >= tl->Length()) { if (Errors > 0) LgiMsg(this, "Failed to save %i of %i objects.", AppName, MB_OK, Errors, tl->Length()); Quit(); return; } } LProgressDlg::OnPulse(); } }; int ScribeWnd::OnCommand(int Cmd, int Event, OsView WndHandle) { // Send mail multi-menu if (Cmd >= IDM_SEND_FROM && Cmd <= IDM_SEND_FROM + (ssize_t)Accounts.Length()) { Send(Cmd - IDM_SEND_FROM); return 0; } // Receive mail multi-menu if (Cmd >= IDM_RECEIVE_FROM && Cmd < IDM_RECEIVE_FROM + (ssize_t)Accounts.Length()) { Receive(Cmd - IDM_RECEIVE_FROM); return 0; } // Preview mail multi-menu if (Cmd >= IDM_PREVIEW_FROM && Cmd < IDM_PREVIEW_FROM + (ssize_t)Accounts.Length()) { Preview(Cmd - IDM_PREVIEW_FROM); return 0; } // Identity multi-menu if (Cmd >= IDM_IDENTITY_BASE && Cmd <= IDM_IDENTITY_BASE + (ssize_t)Accounts.Length()) { SetCurrentIdentity(Cmd - IDM_IDENTITY_BASE - 1); return 0; } // Is this a script tool? if (LScribeScript::Inst && Cmd >= IDM_TOOL_SCRIPT_BASE && Cmd < IDM_TOOL_SCRIPT_BASE + (int)d->Scripts.Length()) { // Do tools menu callback... find the right callback.... LArray c; if (GetScriptCallbacks(LToolsMenu, c)) { for (unsigned i=0; iFunc && c[i]->Param == Cmd) { // Call the callback char Msg[MAX_PATH_LEN]; LScribeScript::Inst->GetLog()->Write ( Msg, sprintf_s(Msg, sizeof(Msg), "\n\nRunning tool script '%s'...\n", c[i]->Script->Code->GetFileName()) ); // Setup the arguments... LScriptArguments Args(NULL); Args.New() = new LVariant((LDom*)this); Args.New() = new LVariant(Cmd); // Call the method ExecuteScriptCallback(*c[i], Args); // Cleanup Args.DeleteObjects(); break; } } } return 0; } // New from template multi-menu if (Cmd >= IDM_NEW_FROM_TEMPLATE && Cmd < IDM_NEW_FROM_TEMPLATE + 100) { int Index = Cmd - IDM_NEW_FROM_TEMPLATE; ScribeFolder *Templates = GetFolder(FOLDER_TEMPLATES); if (Templates) { Templates->LoadThings(); for (auto i: Templates->Items) { Mail *m = i->IsMail(); if (m) { if (Index == 0) { Thing *t = CreateItem(MAGIC_MAIL, 0, false); // GetFolder(FOLDER_OUTBOX) Mail *NewMail = IsMail(t); if (NewMail) { *NewMail = (Thing&)*m; NewMail->DoUI(); break; } } Index--; } } } return 0; } switch (Cmd) { // File menu case IDM_MANAGE_MAIL_STORES: { auto Dlg = new ManageMailStores(this); Dlg->DoModal([this, Dlg](auto dlg, auto id) { LAutoPtr mem(dlg); if (id) { SaveOptions(); if (!UnLoadFolders()) return; LXmlTag *Ms = GetOptions()->LockTag(OPT_MailStores, _FL); if (Ms) { while (Ms->Children.Length()) delete Ms->Children[0]; LXmlTag *t = Dlg->Options.GetChildTag(OPT_MailStores); if (t) { for (auto c: t->Children) { LXmlTag *n = new LXmlTag; n->Copy(*c, true); Ms->InsertTag(n); } } GetOptions()->Unlock(); } LVariant v; GetOptions()->SetValue(OPT_CreateFoldersIfMissing, v = true); if (!Dlg->Options.GetValue(OPT_StartInFolder, v)) v.Empty(); GetOptions()->SetValue(OPT_StartInFolder, v); LoadFolders(NULL); } }); break; } case IDM_REPLICATE: { auto Dlg = new ReplicateDlg(this); Dlg->DoModal([this, Dlg](auto dlg, auto id) { if (id) { UnLoadFolders(); Dlg->StartProcess(); // Don't delete dialog... let it run } else delete dlg; }); break; } case IDM_SECURITY: { // Check for user perm password... // No point allow any old one to edit the security settings. auto ShowDialog = [this]() { auto Dlg = new SecurityDlg(this); Dlg->DoModal(NULL); }; LPassword p; if (p.Serialize(GetOptions(), OPT_UserPermPassword, false)) { GetAccessLevel(this, PermRequireUser, "Security Settings", [ShowDialog](bool Allow) { if (Allow) ShowDialog(); }); } else { ShowDialog(); } break; } case IDM_OPTIONS: { LVariant ShowTotals; GetOptions()->GetValue(OPT_ShowFolderTotals, ShowTotals); // do the dialog auto Dlg = new OptionsDlg(this); Dlg->DoModal([this, Dlg, ShowTotals](auto dlg, auto id) { if (id) { // set up the POP3 accounts SetupAccounts(); SaveOptions(); // close any IMAP accounts that are now disabled. for (auto a : Accounts) { if (a->Receive.IsConfigured() && a->Receive.IsPersistant()) { if (a->Receive.Disabled()) a->Receive.Disconnect(); else Receive(a->GetIndex()); } } // List/Tree view options update LVariant i; if (GetOptions()->GetValue(OPT_ShowFolderTotals, i) && i.CastInt32() != ShowTotals.CastInt32()) { Tree->UpdateAllItems(); } if (GetOptions()->GetValue(OPT_PreviewLines, i)) { Mail::PreviewLines = i.CastInt32() != 0; } if (MailList) { if (GetOptions()->GetValue(OPT_GridLines, i)) { MailList->DrawGridLines(i.CastInt32() != 0); } MailList->Invalidate(); } // date formats if (GetOptions()->GetValue(OPT_DateFormat, i)) { int Idx = i.CastInt32(); if (Idx >= 0 && Idx < CountOf(DateTimeFormats)) { LDateTime::SetDefaultFormat(DateTimeFormats[Idx]); } } if (GetOptions()->GetValue(OPT_AdjustDateTz, i)) Mail::AdjustDateTz = i.CastInt32() == 0; // SSL debug logging if (GetOptions()->GetValue(OPT_DebugSSL, i)) SslSocket::DebugLogging = i.CastInt32() != 0; // Html edit menu if (GetOptions()->GetValue(OPT_EditControl, i)) { auto mi = Menu->FindItem(IDM_HTML_EDITOR); if (mi) mi->Checked(i.CastInt32() != 0); } } delete dlg; }); break; } case IDM_WORK_OFFLINE: { if (WorkOffline) { WorkOffline->Checked(!WorkOffline->Checked()); LVariant v; GetOptions()->SetValue(OPT_WorkOffline, v = WorkOffline->Checked()); if (!WorkOffline->Checked()) { // Offline -> Online transition. // Check if any pending messages are in the Outbox ScribeFolder *Outbox = GetFolder(FOLDER_OUTBOX); if (Outbox) { bool HasMailToSend = false; for (auto t: Outbox->Items) { Mail *m = t->IsMail(); if (m) { if (TestFlag(m->GetFlags(), MAIL_READY_TO_SEND)) { HasMailToSend = true; break; } } } if (HasMailToSend) { PostEvent(M_COMMAND, IDM_SEND_MAIL, #ifndef __GTK_H__ (LMessage::Param)Handle() #else 0 #endif ); } } } } break; } case IDM_ITEM_FILTER: { if (GetCtrlValue(IDM_ITEM_FILTER)) { if ((SearchView = new LSearchView(this))) { SearchView->Focus(true); SetLayout(); } } else { DeleteObj(SearchView); } ScribeFolder *Folder = GetCurrentFolder(); if (Folder) { Folder->Populate(MailList); } break; } case IDM_PRINT: { if (MailList) { List Sel; if (MailList->LList::GetSelection(Sel)) { for (auto i: Sel) { ThingType *t = dynamic_cast(i); ThingPrint(NULL, t); } } } break; } case IDM_PRINTSETUP: { auto *p = GetPrinter(); if (p && p->Browse(this)) { LString Str; if (p->Serialize(Str, true)) { LVariant v; GetOptions()->SetValue(OPT_PrintSettings, v = Str); } } break; } case IDM_PAGE_SETUP: { auto Dlg = new ScribePageSetup(this, GetOptions()); Dlg->DoModal(NULL); break; } case IDM_EXIT: { LMouse m; GetMouse(m); d->IngoreOnClose = m.Ctrl(); LCloseApp(); break; } // Edit menu case IDM_FIND: { auto v = GetFocus(); LDocView *doc = dynamic_cast(v); if (doc) { doc->DoFind(NULL); } else { ScribeFolder *Folder = GetCurrentFolder(); OpenFinder(this, Folder); } break; } case IDM_COPY: { if (MailList && MailList->Focus()) { List Lst; if (!MailList->GetSelection(Lst)) break; // Copy 'ScribeThingList' ScribeClipboardFmt *tl = ScribeClipboardFmt::Alloc(Lst); if (!tl) break; LClipBoard Clip(this); if (Clip.IsOpen()) { if (!Clip.Binary(d->ClipboardFormat, (uchar*)tl, tl->Sizeof(), true)) { LgiMsg(this, "Couldn't set the clipboard data.", AppName, MB_OK); } } else { LgiMsg(this, "Couldn't open the clipboard.", AppName, MB_OK); } free(tl); } else { LViewI *v = LAppInst->GetFocus(); if (v) v->PostEvent(M_COPY); } break; } case IDM_PASTE: { LViewI *v = LAppInst->GetFocus(); if (v && v->GetWindow() != (LWindow*)this) { v->PostEvent(M_PASTE); break; } if (!MailList->Focus() && !Tree->Focus()) { LgiTrace("%s:%i - List/Tree doesn't have focus.\n"); break; } ScribeFolder *Folder = dynamic_cast(Tree->Selection()); if (!Folder || !Folder->GetObject()) { LgiMsg(this, "No current folder.", AppName, MB_OK); break; } LClipBoard Clip(this); if (!Clip.IsOpen()) { LgiMsg(this, "Couldn't open the clipboard.", AppName, MB_OK); break; } LAutoPtr Data; ssize_t Size = 0; if (!Clip.Binary(d->ClipboardFormat, Data, &Size)) { LgiMsg(this, "Couldn't get the clipboard data.", AppName, MB_OK); break; } if (ScribeClipboardFmt::IsThing(Data.Get(), Size)) { new ScribePasteState(this, Folder, Data, Size); } break; } case IDM_DELETE: { LViewI *f = LAppInst->GetFocus(); LEdit *e = dynamic_cast(f); if (e) { // This handles the case where on a mac the menu eats the delete key, even // when the edit control needs it LKey k(LK_DELETE, 0); k.Down(true); f->OnKey(k); k.Down(false); f->OnKey(k); } else { OnDelete(); } break; } case IDM_DELETE_AS_SPAM: { if (MailList) { List Sel; MailList->GetSelection(Sel); int Index = -1; for (auto i: Sel) { Mail *m = IsMail(i); if (m) { if (Index < 0) { Index = MailList->IndexOf(i); } m->DeleteAsSpam(this); } } if (Index >= 0) { LListItem *i = MailList->ItemAt(Index); if (!i) i = MailList->ItemAt(MailList->Length()-1); if (i) i->Select(true); } } break; } case IDM_REFRESH: { ScribeFolder *f = GetCurrentFolder(); if (!f) break; const char *s = DomToStr(SdRefresh); f->GetFldObj()->OnCommand(s); break; } // Mail menu case IDM_NEW_EMAIL: { CreateMail(); break; } case IDM_SET_READ: case IDM_SET_UNREAD: { ScribeFolder *f = GetCurrentFolder(); if (!f) break; bool SetRead = Cmd == IDM_SET_READ; f->LoadThings(); LArray Change; for (auto t: f->Items) { Mail *m = t->IsMail(); if (m && m->Select()) Change.Add(m->GetObject()); } LVariant v = MAIL_READ; LDataStoreI *Store = f->GetObject()->GetStore(); if (Store->Change(Change, FIELD_FLAGS, v, SetRead ? OpPlusEquals : OpMinusEquals) == Store3Error) { for (auto t : f->Items) { Mail *m = t->IsMail(); if (!m) continue; if (!m->Select()) continue; if (SetRead) m->SetFlags(m->GetFlags() | MAIL_READ); else m->SetFlags(m->GetFlags() & ~MAIL_READ); } } break; } case IDM_REPLY: case IDM_REPLY_ALL: { if (MailList) MailReplyTo(IsMail(MailList->GetSelected()), (Cmd == IDM_REPLY_ALL)); break; } case IDM_FORWARD: { if (MailList) MailForward(IsMail(MailList->GetSelected())); break; } case IDM_BOUNCE: { if (MailList) MailBounce(IsMail(MailList->GetSelected())); break; } case IDM_SEND_MAIL: { Send(); break; } case IDM_RECEIVE_AND_SEND: { d->SendAfterReceive = true; PostEvent(M_COMMAND, IDM_RECEIVE_MAIL, (LMessage::Param)FindControl(IDM_RECEIVE_MAIL)); break; } case IDM_THREAD: { if (MailList) { ScribeFolder *f = GetCurrentFolder(); if (f) { f->SetThreaded(!f->GetThreaded()); f->Populate(MailList); } } break; } case IDM_RECEIVE_ALL: { #define LOG_RECEIVE_ALL 0 int i = 0; Accounts.Sort(AccountCmp); for (auto a : Accounts) { #if LOG_RECEIVE_ALL auto name = a->Identity.Name(); auto email = a->Identity.Email(); LString desc; desc.Printf("%s/%s", name.Str(), email.Str()); #endif if (!a->Receive.IsConfigured()) { #if LOG_RECEIVE_ALL LgiTrace("%s:%i - %i/%s not configured.\n", _FL, a->GetIndex(), desc.Get()); #endif } else if (a->Receive.Disabled() > 0) { #if LOG_RECEIVE_ALL LgiTrace("%s:%i - %i/%s is disabled.\n", _FL, a->GetIndex(), desc.Get()); #endif } else { #if LOG_RECEIVE_ALL LgiTrace("%s:%i - %i/%s will connect.\n", _FL, a->GetIndex(), desc.Get()); #endif Receive(a->GetIndex()); } i++; } break; } case IDM_RECEIVE_MAIL: { LVariant Def; if (GetOptions()->GetValue(OPT_Pop3DefAction, Def) && Def.CastInt32() == 0) return OnCommand(IDM_RECEIVE_ALL, 0, NULL); Receive(0); break; } case IDM_PREVIEW_POP3: { LArray Account; Accounts.Sort(AccountCmp); for (auto a: Accounts) { if (!a->Receive.IsConfigured()) continue; auto Protocol = ProtocolStrToEnum(a->Receive.Protocol().Str()); if (Protocol == ProtocolPop3) { Account.Add(a); break; } } if (Account.Length() == 1) OpenPopView(this, Account); break; } case IDM_CALENDAR: { extern void OpenCalender(ScribeFolder *folder); ScribeFolder *Folder = GetFolder(FOLDER_CALENDAR); if (Folder) { OpenCalender(Folder); } break; } // Contact menu case IDM_NEW_CONTACT: { CreateItem(MAGIC_CONTACT, NULL); break; } case IDM_NEW_GROUP: { CreateItem(MAGIC_GROUP, NULL); break; } // Filter menu case IDM_NEW_FILTER: { Thing *t = CreateItem(MAGIC_FILTER, NULL, false); if (t) { t->IsFilter()->SetIncoming(true); t->DoUI(); } break; } case IDM_FILTER_CURRENT_FOLDER: { ScribeFolder *Folder = GetCurrentFolder(); if (Folder) { List Filters; GetFilters(Filters, false, false, true); List Src; for (auto i: Folder->Items) { if (i->IsMail()) { Src.Insert(i->IsMail()); } } if (!Src[0]) { LgiMsg(this, LLoadString(IDS_NO_MAIL_TO_FILTER), AppName); } else { Filter::ApplyFilters(this, Filters, Src); } } break; } case IDM_FILTER_SELECTION: { ScribeFolder *Folder = GetCurrentFolder(); if (Folder) { List Filters; GetFilters(Filters, false, false, true); List Src; for (auto i: Folder->Items) { if (i->IsMail() && i->Select()) { Src.Insert(i->IsMail()); } } if (Src.Length()) { Filter::ApplyFilters(this, Filters, Src); } } break; } case IDM_DEBUG_FILTERS: { auto i = Menu->FindItem(IDM_DEBUG_FILTERS); if (i) { i->Checked(!i->Checked()); } break; } case IDM_HTML_EDITOR: { auto i = Menu->FindItem(IDM_HTML_EDITOR); if (i) { i->Checked(!i->Checked()); LVariant v; GetOptions()->SetValue(OPT_EditControl, v = i->Checked() ? 1 : 0); } break; } case IDM_FILTERS_DISABLE: { if (d->DisableUserFilters) { d->DisableUserFilters->Checked(!d->DisableUserFilters->Checked()); LVariant v; GetOptions()->SetValue(OPT_DisableUserFilters, v = d->DisableUserFilters->Checked()); } break; } case IDM_BUILD_BAYES_DB: { BuildSpamDb(); break; } case IDM_BAYES_STATS: { BuildStats(); break; } case IDM_BAYES_SETTINGS: { auto Dlg = new BayesDlg(this); Dlg->DoModal([this, Dlg](auto dlg, auto id) { if (id) { LVariant i; if (GetOptions()->GetValue(OPT_BayesFilterMode, i)) { ScribeBayesianFilterMode m = ((ScribeBayesianFilterMode)i.CastInt32()); if (m != BayesOff) { LVariant SpamPath, ProbablyPath; GetOptions()->GetValue(OPT_SpamFolder, SpamPath); GetOptions()->GetValue(OPT_BayesMoveTo, ProbablyPath); if (m == BayesFilter) { ScribeFolder *Spam = GetFolder(SpamPath.Str()); if (!Spam) { LMailStore *RelevantStore = GetMailStoreForPath(SpamPath.Str()); if (RelevantStore) { LString p = SpamPath.Str(); LString::Array a = p.SplitDelimit("/"); Spam = RelevantStore->Root; for (unsigned i=1; iGetSubFolder(a[i]); if (!c) c = Spam->CreateSubDirectory(a[i], MAGIC_MAIL); Spam = c; } } } if (Spam) { LVariant v; GetOptions()->SetValue(OPT_HasSpam, v = 1); } } else if (m == BayesTrain) { ScribeFolder *Probably = GetFolder(ProbablyPath.Str()); if (!Probably) { LgiMsg(this, "Couldn't find the folder '%s'", AppName, MB_OK, ProbablyPath.Str()); } } } } } delete dlg; }); break; } case IDM_BAYES_CHECK: { List Sel; if (MailList) MailList->GetSelection(Sel); for (auto i: Sel) { Thing *t = dynamic_cast(i); if (t) { Mail *m = t->IsMail(); if (m) { d->BayesLog.Empty(); double SpamRating = 0.0; IsSpam(SpamRating, m, true); break; } } } break; } // Tools menu case IDM_SCRIPTING_CONSOLE: case IDM_SHOW_CONSOLE: { ShowScriptingConsole(); if (d->ShowConsoleBtn) d->ShowConsoleBtn->Image(IMG_CONSOLE_NOMSG); break; } case IDM_EXPORT_TEXT_MBOX: { Export_UnixMBox(this); break; } case IDM_IMPORT_CSV: { ImportCsv(this); break; } case IDM_EXPORT_CSV: { ExportCsv(this); break; } case IDM_IMPORT_EML: { ImportEml(this); break; } case IDM_EXPORT_SCRIBE: { ExportScribe(this, NULL/* default mail store */); break; } case IDM_IMPORT_TEXT_MBOX: { Import_UnixMBox(this); break; } case IDM_IMP_EUDORA_ADDR: { Import_EudoraAddressBook(this); break; } case IDM_IMP_MOZILLA_ADDR: { Import_MozillaAddressBook(this); break; } case IDM_IMP_MOZILLA_MAIL: { Import_MozillaMail(this); break; } #if WINNATIVE case IDM_IMPORT_OUTLOOK_PAB: { Import_OutlookContacts(this); break; } case IDM_IMPORT_OUTLOOK_ITEMS: { Import_Outlook(this, IMP_OUTLOOK); break; } case IDM_EXPORT_OUTLOOK_ITEMS: { Export_Outlook(this); break; } #endif case IDM_IMP_MBX_EMAIL: { Import_OutlookExpress(this, false); // v4 break; } case IDM_IMP_DBX_EMAIL: { Import_OutlookExpress(this); // v5 break; } case IDM_IMPORT_NS_CONTACTS: { Import_NetscapeContacts(this); break; } case IDM_CHECK_UPDATE: { LVariant v; GetOptions()->GetValue(OPT_SoftwareUpdateIncBeta, v); SoftwareUpdate(this, true, v.CastInt32() != 0, [](auto goingToUpdate) { if (goingToUpdate) LCloseApp(); }); break; } case IDM_LOGOUT: { CurrentAuthLevel = PermRequireNone; auto i = Menu->FindItem(IDM_LOGOUT); if (i) i->Enabled(false); break; } case IDM_LAYOUT1: { LVariant v; int TwoThirds = GetClient().Y() >> 1; GetOptions()->SetValue(OPT_SplitterPos, v = 200); GetOptions()->SetValue(OPT_SubSplitPos, v = TwoThirds); SetLayout(FoldersListAndPreview); break; } case IDM_LAYOUT2: { LVariant v; int TwoThirds = GetClient().Y() >> 1; GetOptions()->SetValue(OPT_SplitterPos, v = TwoThirds); GetOptions()->SetValue(OPT_SubSplitPos, v = 200); SetLayout(PreviewOnBottom); break; } case IDM_LAYOUT3: { LVariant v; GetOptions()->SetValue(OPT_SplitterPos, v = 200); SetLayout(FoldersAndList); break; } case IDM_LAYOUT4: { LVariant v; GetOptions()->SetValue(OPT_SplitterPos, v = 200); GetOptions()->SetValue(OPT_SubSplitPos, v); SetLayout(ThreeColumn); break; } case IDM_CRASH: { int *Crash = 0; *Crash = true; break; } case IDM_DUMP_MEM: { LDumpMemoryStats(0); break; } case IDM_SCRIPT_DEBUG: { LVariant v; if (GetOptions()) GetOptions()->SetValue(OPT_ScriptDebugger, v = true); LVirtualMachine *vm = new LVirtualMachine(d); if (!vm) break; LVmDebugger *dbg = vm->OpenDebugger(); if (!dbg) break; dbg->OwnVm(true); break; } case IDM_SCRIPT_BREAK_ON_WARN: { auto mi = GetMenu()->FindItem(IDM_SCRIPT_BREAK_ON_WARN); if (!mi) break; LVirtualMachine::BreakOnWarning = !mi->Checked(); mi->Checked(LVirtualMachine::BreakOnWarning); break; } case IDM_UNIT_TESTS: { #ifdef _DEBUG UnitTests([this](auto ok) { LgiMsg(this, "UnitTest status: %i", AppName, MB_OK, ok); }); #endif break; } // Help menu case IDM_HELP: { LaunchHelp("index.html"); // LgiMsg(this, LLoadString(IDS_ERROR_NO_HELP), AppName, MB_OK); break; } case IDM_FEEDBACK: { LVariant e; if (GetOptions()->GetValue("author", e)) CreateMail(0, e.Str()); else CreateMail(0, AuthorEmailAddr); break; } case IDM_MEMECODE: { LExecute(AuthorHomepage); break; } case IDM_HOMEPAGE: { LVariant hp; if (GetOptions()->GetValue("homepage", hp)) LExecute(hp.Str()); else LExecute(ApplicationHomepage); break; } case IDM_VERSION_HISTORY: { LExecute("http://www.memecode.com/site/ver.php?id=445"); break; } case IDM_DEBUG_INFO: { char s[256]; sprintf_s(s, sizeof(s), "%s#debug", ApplicationHomepage); LExecute(s); break; } case IDM_TUTORIALS: { LExecute("http://www.memecode.com/scribe/tutorials"); break; } case IDM_INSCRIBE_LINK: { LExecute(CommercialHomepage); break; } case IDM_SCRIBE_FAQ: { LExecute(FaqHomepage); break; } case IDM_ABOUT: { extern void ScribeAbout(ScribeWnd *Parent); ScribeAbout(this); break; } default: { if (d->ScriptToolbar) d->ScriptToolbar->ExecuteCallbacks(this, 0, 0, Cmd); break; } } return 0; } void ScribeWnd::OnDelete() { LVariant ConfirmDelete; GetOptions()->GetValue(OPT_ConfirmDelete, ConfirmDelete); if (!ConfirmDelete.CastInt32() || LgiMsg(this, LLoadString(IDS_DELETE_ASK), AppName, MB_YESNO) == IDYES) { LArray Del; if (Tree && Tree->Focus()) { ScribeFolder *Item = dynamic_cast(Tree->Selection()); if (Item) { Tree->OnDelete(Item, false); } } else if (MailList #ifdef MAC && MailList->Focus() #endif ) { List Sel; MailList->GetSelection(Sel); for (auto i: Sel) { Thing *t = dynamic_cast(i); if (t) Del.Add(t->GetObject()); } if (Del.Length()) { auto Store = Del[0]->GetStore(); Store->Delete(Del, true); } else LgiTrace("%s:%i - Nothing to delete\n", _FL); #ifndef MAC MailList->Focus(true); #endif } } } int ScribeWnd::OnNotify(LViewI *Ctrl, LNotification n) { switch (Ctrl->GetId()) { case IDC_THING_LIST: { if (n.Type == LNotifyReturnKey) { LListItem *i = MailList ? MailList->GetSelected() : 0; Thing *t = dynamic_cast(i); if (t) { t->DoUI(); } } else if (n.Type == LNotifyDeleteKey) { /* This is now handled by the menu OnDelete(); return true; */ } if (SearchView && MailList) { SearchView->OnNotify(Ctrl, n); } break; } case IDC_TEXT: { if (PreviewPanel) { PreviewPanel->OnNotify(Ctrl, n); } break; } } return 0; } void ScribeWnd::AddThingSrc(ScribeFolder *src) { if (!d->ThingSources.HasItem(src)) d->ThingSources.Add(src); } void ScribeWnd::RemoveThingSrc(ScribeFolder *src) { d->ThingSources.Delete(src); } LArray ScribeWnd::GetThingSources(Store3ItemTypes Type) { LArray a; for (auto f: d->ThingSources) { if (f->GetItemType() == Type && !f->IsInTrash()) { a.Add(f); } } return a; } bool ScribeWnd::LogFilterActivity() { auto i = Menu->FindItem(IDM_DEBUG_FILTERS); return i ? i->Checked() : false; } bool ScribeWnd::CreateFolders(LAutoString &FileName) { bool Status = false; if (FileName) { char *Ext = LGetExtension(FileName); if (!Ext) { char File[300]; strcpy_s(File, sizeof(File), FileName); strcat(File, ".mail3"); FileName.Reset(NewStr(File)); } // Create objects, and then close the file.. it'll be reloaded later LAutoPtr m(CreateDataStore(FileName, true)); if (m) { m->GetRoot(true); Status = true; } else LgiTrace("%s:%i - CreateDataStore failed.\n", _FL); } else LgiTrace("%s:%i - No file name for CreateFolder.\n", _FL); return Status; } bool ScribeWnd::CompactFolders(LMailStore &Store, bool Interactive) { if (!Store.Store) return false; auto Dlg = new Store3Progress(this, Interactive); Dlg->SetDescription(LLoadString(IDS_CHECKING_OBJECTS)); bool Offline = false; if (WorkOffline) { Offline = WorkOffline->Checked(); WorkOffline->Checked(true); } Store.Store->Compact(this, Dlg, [this, Offline, Dlg](auto status) { LAssert(InThread()); if (WorkOffline) WorkOffline->Checked(Offline); delete Dlg; }); return true; } CalendarSource *CalendarSource::Create(ScribeWnd *App, const char *ObjName, const char *Id) { if (!Stricmp(ObjName, "RemoteCalendarSource")) return new RemoteCalendarSource(App, Id); return new FolderCalendarSource(App, Id); } int ScribeWnd::GetCalendarSources(LArray &Out) { static bool Loaded = false; if (!Loaded) { Loaded = true; CalendarSource::SetCreateIn(NULL); LVariant Create; GetOptions()->GetValue(OPT_CalendarCreateIn, Create); // This should be a list of all calendar folders in ANY mail store... auto CalFlds = GetThingSources(MAGIC_CALENDAR); LXmlTag *t = GetOptions()->LockTag(OPT_CalendarSources, _FL); if (t) { bool AutoPopulate = t->Children.Length() == 0; for (auto c: t->Children) { auto s = CalendarSource::Create(this, c->GetAttr(CalendarSource::OptObject), c->GetTag()); if (s && s->Read()) { // Add known source... if (!Stricmp(Create.Str(), c->GetTag())) { CalendarSource::SetCreateIn(s); } // Remove from CalFlds FolderCalendarSource *Fcs = dynamic_cast(c); if (Fcs) { auto Path = Fcs->GetPath(); for (auto c: CalFlds) { if (c->GetPath().Equals(Path)) { CalFlds.Delete(c); break; } } } } } if (AutoPopulate) { // Now CalFlds should be a list of all calendar folders NOT in the source XML tag for (auto c: CalFlds) { FolderCalendarSource *s = new FolderCalendarSource(this); if (s) { // So add an entry to track it... auto Path = c->GetPath(); s->SetPath(Path); s->SetDisplay(true); s->SetColour(CalendarSource::FindUnusedColour()); s->Write(); } } } GetOptions()->Unlock(); } if (!CalendarSource::GetCreateIn() && CalendarSource::GetSources().Length()) { CalendarSource::SetCreateIn(CalendarSource::GetSources().ItemAt(0)); } } for (unsigned i=0; iPathOption; fi++) { bool Check = true; if (fi->HasOption) { LVariant v; if (GetOptions()->GetValue(fi->HasOption, v)) Check = v.CastInt32() != 0; } if (Check) { ScribeFolder *c = GetFolder(fi->Id); if (c == f) return fi->Id; } } return -1; } ScribeFolder *ScribeWnd::GetCurrentFolder() { if (Tree) { auto *Item = Tree->Selection(); if (Item) { return dynamic_cast(Item); } } return 0; } bool ScribeWnd::GetSystemPath(int Folder, LVariant &Path) { char KeyName[64]; sprintf_s(KeyName, sizeof(KeyName), "Folder-%i", Folder); return GetOptions()->GetValue(KeyName, Path); } LMailStore *ScribeWnd::GetMailStoreForIdentity(const char *IdEmail) { LVariant Tmp; if (!IdEmail) { // Get current identity ScribeAccount *Cur = GetCurrentAccount(); if (Cur) { Tmp = Cur->Identity.Email(); IdEmail = Tmp.Str(); } } if (!IdEmail) return NULL; ScribeAccount *a = NULL; for (auto Acc : Accounts) { LVariant e = Acc->Identity.Email(); if (e.Str() && !_stricmp(e.Str(), IdEmail)) { a = Acc; break; } } if (!a) return NULL; LVariant DestPath = a->Receive.DestinationFolder(); if (!DestPath.Str()) return NULL; return GetMailStoreForPath(DestPath.Str()); } ScribeFolder *ScribeWnd::GetFolder(int Id, LDataI *s) { if (s) { for (auto &f: Folders) if (s->GetStore() == f.Store) return GetFolder(Id, &f); } return GetFolder(Id); } ScribeFolder *ScribeWnd::GetFolder(int Id, LMailStore *Store, bool Quiet) { char KeyName[64]; sprintf_s(KeyName, sizeof(KeyName), "Folder-%i", Id); LVariant FolderName; bool NoOption = false; if (GetOptions()->GetValue(KeyName, FolderName)) { if (ValidStr(FolderName.Str()) && strlen(FolderName.Str()) > 0) { ScribeFolder *c = GetFolder(FolderName.Str(), Store); if (c) { return c; } else if (!Quiet) { LgiTrace("%s:%i - '%s' doesn't exist.\n", _FL, FolderName.Str()); } } } else if (!Quiet) { // LgiTrace("%s:%i - No option '%s'\n", _FL, KeyName); NoOption = true; } switch (Id) { case FOLDER_INBOX: case FOLDER_OUTBOX: case FOLDER_SENT: case FOLDER_TRASH: case FOLDER_CONTACTS: case FOLDER_TEMPLATES: case FOLDER_FILTERS: case FOLDER_CALENDAR: case FOLDER_GROUPS: case FOLDER_SPAM: { ScribeFolder *c = GetFolder(DefaultFolderNames[Id], Store); if (!c) { // if (!Quiet) // LgiTrace("%s:%i - Default folder '%s' doesn't exist.\n", _FL, DefaultFolderNames[Id]); } else if (NoOption) { auto p = c->GetPath(); GetOptions()->SetValue(KeyName, FolderName = p.Get()); } return c; } } return NULL; } bool ScribeWnd::OnMailStore(LMailStore **MailStore, bool Add) { if (!MailStore) { LAssert(!"No mail store pointer?"); return false; } if (Add) { *MailStore = &Folders.New(); if (*MailStore) return true; } else { ssize_t Idx = *MailStore - &Folders[0]; if (Idx >= 0 && Idx < (ssize_t)Folders.Length()) { Folders.DeleteAt(Idx, true); *MailStore = NULL; return true; } else { LAssert(!"Index out of range."); } } return false; } LMailStore *ScribeWnd::GetMailStoreForPath(const char *Path) { if (!Path) return NULL; auto t = LString(Path).SplitDelimit("/"); if (t.Length() > 0) { const char *First = t[0]; // Find the mail store that that t[0] refers to for (unsigned i=0; iGetText(); if (RootStr && !_stricmp(RootStr, First)) { return &Folders[i]; } } } } return NULL; } ScribeFolder *ScribeWnd::GetFolder(const char *Name, LMailStore *s) { ScribeFolder *Folder = 0; if (ValidStr(Name)) { LString Sep("/"); auto t = LString(Name).Split(Sep); LMailStore tmp; LString TmpName; if (t.Length() > 0) { if (!s) { s = GetMailStoreForPath(Name); if (!s) { // IMAP folders? for (auto a: Accounts) { ScribeProtocol Proto = a->Receive.ProtocolType(); if (Proto == ProtocolImapFull) { ScribeFolder *Root = a->Receive.GetRootFolder(); if (Root) { const char *RootStr = Root->GetText(); if (RootStr && a->Receive.GetDataStore() && !_stricmp(RootStr, t[0])) { tmp.Root = Root; tmp.Store = a->Receive.GetDataStore(); s = &tmp; break; } } } } } if (s) { if (*Name == '/') Name++; Name = strchr(Name, '/'); if (!Name) Name = "/"; } } else if (s->Root) { // Check if the store name is on the start of the folder auto RootName = s->Root->GetName(true); if (RootName.Equals(t[0])) { LString::Array a; for (unsigned i=1; iRoot; Folder = s->Root ? s->Root->GetSubFolder(Name) : 0; } } return Folder; } void ScribeWnd::Update(int What) { if (What & UPDATE_TREE) { Tree->Invalidate(); return; } if (What & UPDATE_LIST) { if (MailList) MailList->Invalidate(); return; } } void ScribeWnd::DoDebug(char *s) { } Thing *ScribeWnd::CreateThingOfType(Store3ItemTypes Type, LDataI *obj) { Thing *t = NULL; switch (Type) { case MAGIC_CONTACT: { t = new Contact(this, obj); break; } case MAGIC_MAIL: { t = new Mail(this, obj); break; } case MAGIC_ATTACHMENT: { t = new Attachment(this, obj); break; } case MAGIC_FILTER: { t = new Filter(this, obj); break; } case MAGIC_CALENDAR: { t = new Calendar(this, obj); break; } case MAGIC_GROUP: { t = new ContactGroup(this, obj); break; } default: break; } if (t) { t->App = this; } return t; } void ScribeWnd::GetFilters(List &Filters, bool JustIn, bool JustOut, bool JustInternal) { auto Srcs = GetThingSources(MAGIC_FILTER); for (auto f: Srcs) { for (auto t: f->Items) { Filter *Ftr = t->IsFilter(); if (Ftr) { if (JustIn && !Ftr->GetIncoming()) continue; if (JustOut && !Ftr->GetOutgoing()) continue; if (JustInternal && !Ftr->GetInternal()) continue; Filters.Insert(Ftr); } } } extern int FilterCompare(Filter *a, Filter *b, NativeInt Data); Filters.Sort(FilterCompare); } bool ScribeWnd::ShowToolbarText() { LVariant i; if (GetOptions()->GetValue(OPT_ToolbarText, i)) { return i.CastInt32() != 0; } GetOptions()->SetValue(OPT_ToolbarText, i = true); return true; } void ScribeWnd::HashContacts(LHashTbl,Contact*> &Contacts, ScribeFolder *Folder, bool Deep) { if (!Folder) { // Default item is the contacts folder Folder = GetFolder(FOLDER_CONTACTS); // Also look at all the contact sources... auto Srcs = GetThingSources(MAGIC_CONTACT); for (auto Src: Srcs) { for (auto t: Src->Items) { Contact *c = t->IsContact(); if (!c) continue; auto emails = c->GetEmails(); for (auto e: emails) { if (!Contacts.Find(e)) Contacts.Add(e, c); } } } } // recurse through each folder and make a list // of every contact object we find. if (Folder) { Folder->LoadThings(); for (auto t: Folder->Items) { Contact *c = t->IsContact(); if (c) { auto Emails = c->GetEmails(); for (auto e: Emails) if (e && !Contacts.Find(e)) Contacts.Add(e, c); } } for (auto f = Folder->GetChildFolder(); Deep && f; f = f->GetNextFolder()) { HashContacts(Contacts, f, Deep); } } } List *ScribeWnd::GetEveryone() { return &Contact::Everyone; } bool ScribeWnd::GetContacts(List &Contacts, ScribeFolder *Folder, bool Deep) { LArray Folders; if (!Folder) { Folders = GetThingSources(MAGIC_CONTACT); auto f = GetFolder(FOLDER_CONTACTS); if (f && !Folders.HasItem(f)) Folders.Add(f); } else Folders.Add(Folder); if (!Folders.Length()) return false; for (auto f: Folders) { // recurse through each folder and make a list // of every contact object we find. ScribePerm Perm = f->GetFolderPerms(ScribeReadAccess); bool Safe = CurrentAuthLevel >= Perm; if (Safe) { f->LoadThings(); for (auto t: f->Items) { Contact *c = t->IsContact(); if (c) Contacts.Insert(c); } for (ScribeFolder *c = f->GetChildFolder(); Deep && c; c = c->GetNextFolder()) GetContacts(Contacts, c, Deep); } } return true; } /* This function goes through the database and checks for some basic requirements and fixes things up if they aren't ok. */ bool ScribeWnd::ValidateFolder(LMailStore *s, int Id) { char OptName[32]; sprintf_s(OptName, sizeof(OptName), "Folder-%i", Id); LVariant Path; if (!GetOptions()->GetValue(OptName, Path)) { char Opt[256]; sprintf_s(Opt, sizeof(Opt), "/%s", DefaultFolderNames[Id]); GetOptions()->SetValue(OptName, Path = Opt); } // If the path name has the store name at the start, strip that off... LString Sep("/"); LString::Array Parts = LString(Path.Str()).Split(Sep); if (Parts.Length() > 1) { if (Parts[0].Equals(s->Name)) { Parts.DeleteAt(0, true); Path = Sep.Join(Parts); } else { LMailStore *ms = GetMailStoreForPath(Path.Str()); if (ms) { s = ms; } else { // Most likely the user has renamed something and broken the // path. Lets just error out instead of creating the wrong folder return false; } } } // Now resolve the path... ScribeFolder *Folder = GetFolder(Path.Str(), s); if (!Folder) { char *p = Path.Str(); if (_strnicmp(p, "/IMAP ", 6) != 0) { LAssert(DefaultFolderTypes[Id] != MAGIC_NONE); Folder = s->Root->CreateSubDirectory(*p=='/'?p+1:p, DefaultFolderTypes[Id]); } } if (!Folder) return false; Folder->SetDefaultFields(); return true; } void ScribeWnd::Validate(LMailStore *s) { // Check for all the basic folders int Errors = 0; for (SystemFolderInfo *fi = SystemFolders; fi->PathOption; fi++) { bool Check = true; if (fi->HasOption) { LVariant v; if (GetOptions()->GetValue(fi->HasOption, v)) Check = v.CastInt32() != 0; } if (Check) { if (!ValidateFolder(s, fi->Id)) Errors++; } } if (Errors && LgiMsg(this, "There were errors validating the system folders." "Would you like to review the mail store's system folder paths?", AppName, MB_YESNO) == IDYES) { PostEvent(M_COMMAND, IDM_MANAGE_MAIL_STORES); } } ThingFilter *ScribeWnd::GetThingFilter() { return SearchView; } ScribeAccount *ScribeWnd::GetSendAccount() { LVariant DefSendAcc = 0; if (!GetOptions()->GetValue(OPT_DefaultSendAccount, DefSendAcc)) { for (auto a : Accounts) if (a->Send.Server().Str()) return a; } ScribeAccount *i = Accounts.ItemAt(DefSendAcc.CastInt32()); if (i && i->Send.Server().Str()) return i; return NULL; } LPrinter *ScribeWnd::GetPrinter() { if (!d->PrintOptions) d->PrintOptions.Reset(new LPrinter); return d->PrintOptions; } int ScribeWnd::GetActiveThreads() { int Status = 0; for (auto i: Accounts) { if (i->IsOnline()) { Status++; } } return Status; } class DefaultClientDlg : public LDialog { public: bool DontWarn; DefaultClientDlg(LView *parent) { DontWarn = false; SetParent(parent); LoadFromResource(IDD_WARN_DEFAULT); MoveToCenter(); } int OnNotify(LViewI *Ctrl, LNotification n) { switch (Ctrl->GetId()) { case ID_YES: case ID_NO: { LCheckBox *DW; if (GetViewById(IDC_DONT_WARN, DW)) { DontWarn = DW->Value() != 0; } EndModal(Ctrl->GetId() == ID_YES); break; } } return 0; } }; #if WINNATIVE struct DefaultClient { char DefIcon[MAX_PATH_LEN]; char CmdLine[MAX_PATH_LEN]; char DllPath[MAX_PATH_LEN]; DefaultClient() { auto Exe = LGetExeFile(); sprintf_s(DefIcon, sizeof(DefIcon), "%s,1", Exe.Get()); sprintf_s(CmdLine, sizeof(CmdLine), "\"%s\" /m \"%%1\"", Exe.Get()); LMakePath(DllPath, sizeof(DllPath), Exe, "../ScribeMapi.dll"); } bool IsWindowsXp() { LArray Ver; int Os = LGetOs(&Ver); if ( ( Os == LGI_OS_WIN32 || Os == LGI_OS_WIN64 ) && Ver.Length() > 1 && Ver[0] == 5 && Ver[1] == 1 ) return true; return false; } bool InstallMailto(bool Write) { LAutoPtr mailto = CheckKey(Write, "HKCR\\mailto"); if (!mailto) return false; if (!CheckString(Write, mailto, NULL, "URL:MailTo Protocol")) return false; LAutoPtr deficon = CheckKey(Write, "HKCR\\mailto\\DefaultIcon"); if (!deficon) return false; if (!CheckString(Write, deficon, NULL, DefIcon)) return false; LAutoPtr shell = CheckKey(Write, "HKCR\\mailto\\shell"); if (!shell) return false; if (!CheckString(Write, shell, NULL, "open")) return false; LAutoPtr cmd = CheckKey(Write, "HKCR\\mailto\\shell\\open\\command"); if (!cmd) return false; if (!CheckString(Write, cmd, NULL, CmdLine)) return false; return true; } LAutoPtr CheckKey(bool Write, const char *Key, ...) const { char Buffer[512]; va_list Arg; va_start(Arg, Key); vsprintf_s(Buffer, sizeof(Buffer), Key, Arg); va_end(Arg); LAutoPtr k(new LRegKey(Write, Buffer)); if (k && Write && !k->IsOk()) { if (!k->Create()) { k.Reset(); LgiTrace("%s:%i - Failed to create '%s'\n", _FL, Buffer); } } return k; } bool CheckInt(bool Write, LRegKey *k, const char *Name, uint32_t Value) { if (!k) { LgiTrace("%s:%i - No key: '%s'\n", _FL, Name); return false; } uint32_t Cur; if (!k->GetInt(Name, Cur)) Cur = Value + 1; if (Cur == Value) return true; if (Write) { bool Status = k->SetInt(Name, Value); if (!Status) LgiTrace("%s:%i - Failed to set key '%s': '%s' to %i\n", _FL, k->Name(), Name, Value); return Status; } return false; } bool CheckString(bool Write, LRegKey *k, const char *StrName, const char *StrValue) { if (!k) { LgiTrace("%s:%i - No key: '%s' to '%s'\n", _FL, StrName, StrValue); return false; } LString v; if (k->GetStr(StrName, v)) { bool Same = Stricmp(v.Get(), StrValue) == 0; if (Write && !Same) { bool Status = k->SetStr(StrName, StrValue); if (!Status) LgiTrace("%s:%i - Failed to set key '%s': '%s' to '%s'\n", _FL, k->Name(), StrName, StrValue); return Status; } return Same; } else if (Write) { bool Status = k->SetStr(StrName, StrValue); if (!Status) LgiTrace("%s:%i - Failed to set key '%s': '%s' to '%s'\n", _FL, k->Name(), StrName, StrValue); return Status; } return false; } bool IsDefault() { LAutoPtr mail = CheckKey(false, "HKCU\\Software\\Clients\\Mail"); if (!mail) return false; LString v; if (!mail->GetStr(NULL, v)) return false; return !_stricmp(v, "Scribe"); } bool SetDefault() const { LAutoPtr mail = CheckKey(true, "HKCU\\Software\\Clients\\Mail"); if (!mail) return false; // Set the default client in the current user tree. mail->SetStr(NULL, "Scribe"); // Configure the mailto handler const char *Base = "HKEY_ROOT"; bool Error = false; LRegKey Mt(true, "%s\\mailto", Base); if (Mt.IsOk() || Mt.Create()) { if (!Mt.SetStr(0, "URL:MailTo Protocol") || !Mt.SetStr("URL Protocol", "")) Error = true; } else { LgiTrace("%s:%i - Couldn't open/create registry key (err=%i).\n", _FL, GetLastError()); Error = true; } LRegKey Di(true, "%s\\mailto\\DefaultIcon", Base); if (Di.IsOk() || Di.Create()) { if (!Di.SetStr(0, DefIcon)) Error = true; } else { LgiTrace("%s:%i - Couldn't open/create registry key (err=%i).\n", _FL, GetLastError()); Error = true; } LRegKey c(true, "%s\\mailto\\shell\\open\\command", Base); if (c.IsOk() || c.Create()) { if (!c.SetStr(NULL, CmdLine)) Error = true; } else { LgiTrace("%s:%i - Couldn't open/create registry key (err=%i).\n", _FL, GetLastError()); Error = true; } return Error; } bool InstallAsClient(char *Base, bool Write) { // Create software client entry, to put Scribe in the Internet Options for mail clients. LAutoPtr mail = CheckKey(Write, "%s\\Software\\Clients\\Mail", Base); if (!mail) return false; LAutoPtr app = CheckKey(Write, "%s\\Software\\Clients\\Mail\\Scribe", Base); if (!app) return false; if (!CheckString(Write, app, NULL, AppName)) return false; if (!CheckString(Write, app, "DllPath", DllPath)) return false; LAutoPtr shell = CheckKey(Write, "%s\\Software\\Clients\\Mail\\Scribe\\shell\\open\\command", Base); if (!shell) return false; if (!CheckString(Write, shell, NULL, CmdLine)) return false; LAutoPtr icon = CheckKey(Write, "%s\\Software\\Clients\\Mail\\Scribe\\DefaultIcon", Base); if (!icon) return false; if (!CheckString(Write, icon, NULL, DefIcon)) return false; LAutoPtr proto = CheckKey(Write, "%s\\Software\\Classes\\Protocol\\mailto", Base); if (!proto) return false; if (!CheckString(Write, proto, NULL, "URL:MailTo Protocol")) return false; if (!CheckString(Write, proto, "URL Protocol", "")) return false; if (!CheckInt(Write, proto, "EditFlags", 0x2)) return false; LAutoPtr proto_cmd = CheckKey(Write, "%s\\Software\\Classes\\Protocol\\mailto\\shell\\open\\command", Base); if (!proto_cmd) return false; if (!CheckString(Write, proto_cmd, NULL, CmdLine)) return false; return true; } struct FileType { char *Name; char *Desc; int Icon; }; static FileType FileTypes[]; bool Win7Install(bool Write) { // http://msdn.microsoft.com/en-us/library/windows/desktop/cc144154%28v=vs.85%29.aspx LArray Ver; int Os = LGetOs(&Ver); if ( ( Os == LGI_OS_WIN32 || Os == LGI_OS_WIN64 ) && Ver[0] >= 6) { char Path[MAX_PATH_LEN]; auto Exe = LGetExeFile(); for (int i=0; FileTypes[i].Name; i++) { LAutoPtr base = CheckKey(Write, "HKEY_CLASSES_ROOT\\%s", FileTypes[i].Name); if (!base) return false; if (!CheckString(Write, base, NULL, FileTypes[i].Desc)) return false; LAutoPtr r = CheckKey(Write, "HKEY_CLASSES_ROOT\\%s\\shell\\Open\\command", FileTypes[i].Name); if (!r) return false; sprintf_s(Path, sizeof(Path), "\"%s\" -u \"%%1\"", Exe.Get()); if (!CheckString(Write, r, NULL, Path)) return false; LAutoPtr ico = CheckKey(Write, "HKEY_CLASSES_ROOT\\%s\\DefaultIcon", FileTypes[i].Name); if (!ico) return false; sprintf_s(Path, sizeof(Path), "%s,%i", Exe.Get(), FileTypes[i].Icon); if (!CheckString(Write, ico, NULL, Path)) return false; } LAutoPtr r = CheckKey(Write, "HKEY_LOCAL_MACHINE\\SOFTWARE\\Clients\\Mail\\Scribe\\Capabilities"); if (!r) return false; if (!CheckString(Write, r, "ApplicationDescription", "Scribe is a small lightweight email client.") && !CheckString(Write, r, "ApplicationName", "Scribe") && !CheckString(Write, r, "ApplicationIcon", DefIcon)) return false; LAutoPtr as = CheckKey(Write, "HKEY_LOCAL_MACHINE\\SOFTWARE\\Clients\\Mail\\Scribe\\Capabilities\\FileAssociations"); if (!as) return false; if (!CheckString(Write, as, ".eml", "Scribe.Email") && !CheckString(Write, as, ".msg", "Scribe.Email") && !CheckString(Write, as, ".mbox", "Scribe.Folder") && !CheckString(Write, as, ".mbx", "Scribe.Folder") && !CheckString(Write, as, ".ics", "Scribe.Calendar") && !CheckString(Write, as, ".vcs", "Scribe.Calendar") && !CheckString(Write, as, ".vcf", "Scribe.Contact") && !CheckString(Write, as, ".mail3", "Scribe.MailStore")) return false; LAutoPtr ua = CheckKey(Write, "HKEY_LOCAL_MACHINE\\SOFTWARE\\Clients\\Mail\\Scribe\\Capabilities\\UrlAssociations"); if (!ua) return false; if (!CheckString(Write, ua, "mailto", "Scribe.Mailto")) return false; LAutoPtr a = CheckKey(Write, "HKEY_LOCAL_MACHINE\\SOFTWARE\\RegisteredApplications"); if (!a) return false; if (!CheckString(Write, a, "Scribe", "SOFTWARE\\Clients\\Mail\\Scribe\\Capabilities")) return false; } return true; } void Win7Uninstall() { for (int i=0; FileTypes[i].Name; i++) { LRegKey base(true, "HKEY_CLASSES_ROOT\\%s", FileTypes[i].Name); base.DeleteKey(); } } }; DefaultClient::FileType DefaultClient::FileTypes[] = { { "Scribe.Email", "Email", 2 }, { "Scribe.Folder", "Mailbox", 0 }, { "Scribe.Calendar", "Calendar Event", 6 }, { "Scribe.Contact", "Contact", 4 }, { "Scribe.MailStore", "Mail Store", 0 }, { "Scribe.Mailto", "Mailto Protocol", 0 }, { 0, 0 } }; #endif void ScribeWnd::SetDefaultHandler() { #if WINNATIVE if (LAppInst->GetOption("noreg")) return; LVariant RegisterClient; if (!GetOptions()->GetValue(OPT_RegisterWindowsClient, RegisterClient)) RegisterClient = true; if (!RegisterClient.CastInt32()) return; // Create IE mail client entries for local machine and current user DefaultClient Def; bool OldAssert = LRegKey::AssertOnError; LRegKey::AssertOnError = false; bool RegistryOk = ( !Def.IsWindowsXp() || Def.InstallMailto(true) ) && Def.InstallAsClient("HKLM", true) && Def.Win7Install(true); LRegKey::AssertOnError = OldAssert; if (!RegistryOk) { // Need write permissions to fix up the registry? NeedsCapability("RegistryWritePermissions"); return; } // Check if the user wants us to be the default client LVariant n = true; GetOptions()->GetValue(OPT_CheckDefaultEmail, n); if (n.CastInt32()) { // HKEY_CURRENT_USER\Software\Microsoft\Windows\Shell\Associations\UrlAssociations\mailto\UserChoice LRegKey::AssertOnError = false; bool IsDef = Def.IsDefault(); if (!IsDef) { // Ask the user... auto Dlg = new DefaultClientDlg(this); Dlg->DoModal([this, Dlg, Def, OldAssert](auto dlg, auto id) { if (id) { auto Error = !Def.SetDefault(); LVariant v; GetOptions()->SetValue(OPT_CheckDefaultEmail, v = (int) (!Dlg->DontWarn)); OnSetDefaultHandler(Error, OldAssert); } delete dlg; }); } else OnSetDefaultHandler(false, OldAssert); } #endif } void ScribeWnd::OnSetDefaultHandler(bool Error, bool OldAssert) { #if WINDOWS LRegKey::AssertOnError = OldAssert; #endif if (Error) NeedsCapability("RegistryWritePermissions"); } void ScribeWnd::OnSelect(List *l, bool ChangeEvent) { Mail *m = (l && l->Length() == 1) ? (*l)[0]->IsMail() : 0; if (Commands) { bool NotCreated = m && !TestFlag(m->GetFlags(), MAIL_CREATED); Commands->SetCtrlEnabled(IDM_DELETE, l && l->Length() > 0); Commands->SetCtrlEnabled(IDM_DELETE_AS_SPAM, l && l->Length() > 0); Commands->SetCtrlEnabled(IDM_PRINT, l && l->Length() == 1); Commands->SetCtrlEnabled(IDM_REPLY, NotCreated); Commands->SetCtrlEnabled(IDM_REPLY_ALL, NotCreated); Commands->SetCtrlEnabled(IDM_FORWARD, m != 0); Commands->SetCtrlEnabled(IDM_BOUNCE, m != 0); } if (PreviewPanel && GetEffectiveLayoutMode() != 3) { if (!PreviewPanel->IsAttached()) { SetItemPreview(PreviewPanel); } Thing *t = (l && l->Length() == 1) ? (*l)[0] : 0; PreviewPanel->OnThing(t, ChangeEvent); /* if (d->Debug) d->Debug->OnThing(t); */ } } class SpellErrorInst { public: int Id; LString Word; LString::Array Suggestions; SpellErrorInst(int id) { Id = id; // Decor = LCss::TextDecorSquiggle; // DecorColour.Rgb(255, 0, 0); } ~SpellErrorInst() { } bool OnMenu(LSubMenu *m) { if (Suggestions.Length()) { for (unsigned i=0; iAppendItem(Suggestions[i], 100 + i, true); } m->AppendSeparator(); } char Buf[256]; sprintf_s(Buf, sizeof(Buf), LLoadString(IDS_ADD_TO_DICTIONARY, "Add '%s' to dictionary"), Word.Get()); m->AppendItem(Buf, 1, true); return true; } void OnMenuClick(int i) { if (i == 1) { // Add to dictionary... /* if (PostThreadEvent(SpellHnd, M_ADD_WORD, (LMessage::Param) new LString(Word))) { // FIXME LAssert(!"Impl me."); // View->PostEvent(M_DELETE_STYLE, (LMessage::Param) dynamic_cast(this)); } */ } else if (i >= 100 && i < 100 + (int)Suggestions.Length()) { // Change spelling.. char *Replace = Suggestions[i - 100]; if (Replace) { char16 *w = Utf8ToWide(Replace); if (w) { /* int NewLen = StrlenW(w); if (NewLen > Len) { // Bigger... memcpy(View->NameW() + Start, w, Len * sizeof(char16)); View->Insert(Start + Len, w + Len, NewLen - Len); } else if (NewLen < Len) { // Smaller... memcpy(View->NameW() + Start, w, NewLen * sizeof(char16)); View->Delete(Start + NewLen, Len - NewLen); } else { // Just copy... memcpy(View->NameW() + Start, w, Len * sizeof(char16)); RefreshLayout(Start, Len); } */ DeleteArray(w); } } } } }; class MailTextView : public LTextView3 { ScribeWnd *App; LSpellCheck *Thread; LColour c[8]; LHashTbl, SpellErrorInst*> ErrMap; SpellErrorInst *NewErrorInst() { int Id; while (ErrMap.Find(Id = LRand(10000))) ; SpellErrorInst *Inst = new SpellErrorInst(Id); if (!Inst) return NULL; ErrMap.Add(Id, Inst); return Inst; } public: MailTextView(ScribeWnd *app, int Id, int x, int y, int cx, int cy, LFontType *FontType) : LTextView3(Id, x, y, cx, cy, FontType) { App = app; Thread = 0; int i=0; c[i++].Rgb(0x80, 0, 0); c[i++].Rgb(0, 0x80, 0); c[i++].Rgb(0, 0, 0x80); c[i++].Rgb(0x80, 0x80, 0); c[i++].Rgb(0x80, 0, 0x80); c[i++].Rgb(0, 0x80, 0x80); c[i++].Rgb(0x80, 0x80, 0x80); c[i++].Rgb(0xc0, 0xc0, 0xc0); for (i=0; i 0 && !StrchrW(SpellDelim, Text[Start-1])) Start--; if (Len > 0) { // Text being added Len += Origin - Start; while ((ssize_t)Start + Len < Size && !StrchrW(SpellDelim, Text[Start + Len])) Len++; } else if (Len < 0) { // Text being deleted Len = Origin - Start; while ((ssize_t)Start + Len < Size && !StrchrW(SpellDelim, Text[Start + Len])) Len++; } if (!Thread) Thread = App->GetSpellThread(); if (Thread && Len > 0) { LString Str(Text+Start, Len); LArray Params; Thread->Check(AddDispatch(), Str, Start, Len, &Params); } // Adjust all the positions of the styles after this. for (auto s = Style.begin(); s != Style.end(); ) { if (s->Start >= Origin && s->Owner == 1) { if (Length < 0 && s->Start < Origin - Length) { // In the deleted text... Style.Delete(s); continue; } // After the deleted text s->Start += Length; LAssert(s->Start >= 0); } s++; } } } void PourText(size_t Start, ssize_t Len) { LTextView3::PourText(Start, Len); for (auto l: Line) { int n=0; char16 *t = Text + l->Start; char16 *e = t + l->Len; while ((*t == ' ' || *t == '>') && t < e) if (*t++ == '>') n++; if (n > 0) l->c = c[(n-1)%CountOf(c)]; } } LMessage::Result OnEvent(LMessage *m) { switch (m->Msg()) { case M_CHECK_TEXT: { LAutoPtr Ct((LSpellCheck::CheckText*)m->A()); if (!Ct || !Thread) break; // Clear existing spelling error styles ssize_t Start = Ct->Start; ssize_t End = Start + Ct->Len; for (auto i = Style.begin(); i != Style.end(); ) { if (i->End() < (size_t)Start || i->Start >= End) { // Outside the area we are re-styling. i++; } else { if (i->Owner == STYLE_SPELLING) { // Existing error style inside the area Style.Delete(i); } else { // Existing non-error style... i++; } } } // Insert the new styles for (auto Ct: Ct->Errors) { SpellErrorInst *ErrInst = NewErrorInst(); LAutoPtr Style(new LTextView3::LStyle(STYLE_SPELLING)); if (Style && ErrInst) { Style->View = this; Style->Start = Ct.Start; Style->Len = Ct.Len; Style->Font = GetFont(); Style->Data = ErrInst->Id; Style->DecorColour = LColour::Red; Style->Decor = LCss::TextDecorSquiggle; ErrInst->Word = LString(Text + Style->Start, Style->End()); ErrInst->Suggestions = Ct.Suggestions; InsertStyle(Style); } } // Update the screen... Invalidate(); break; } case M_DELETE_STYLE: { /* LTextView3::LStyle *s = (LTextView3::LStyle*)m->A(); if (s && Style.HasItem(s)) { Style.Delete(s); Invalidate(); } else LAssert(0); */ break; } } return LTextView3::OnEvent(m); } bool OnStyleClick(LStyle *style, LMouse *m) { switch (style->Owner) { case STYLE_URL: { if (m->Left() && m->Down() && m->Double()) { LString s(Text + style->Start, style->Len); LUri u(s); if ( (u.sProtocol && !_stricmp(u.sProtocol, "mailto")) || LIsValidEmail(s) ) { Mailto m(App, s); Mail *email = App->CreateMail(); if (email) { m.Apply(email); email->DoUI(); return true; } } else { // Web link? LExecute(s); } } break; } default: return false; } return true; } }; LDocView *ScribeWnd::CreateTextControl(int Id, const char *MimeType, bool Editor, Mail *m) { LDocView *Ctrl = 0; // Get the default font LFontType FontType; bool UseFont = FontType.Serialize(GetOptions(), OPT_EditorFont, false); if (Editor) { if (!Stricmp(MimeType, sTextHtml)) { // Use the built in html editor LRichTextEdit *Rte; if ((Ctrl = Rte = new LRichTextEdit(Id))) { if (UseFont) Ctrl->SetFont(FontType.Create(), true); // Give the control the speller settings: LVariant Check, Lang, Dict; if (GetOptions()->GetValue(OPT_SpellCheck, Check) && Check.CastInt32() != 0) { if (GetOptions()->GetValue(OPT_SpellCheckLanguage, Lang)) Rte->SetValue(LDomPropToString(SpellCheckLanguage), Lang); if (GetOptions()->GetValue(OPT_SpellCheckDictionary, Dict)) Rte->SetValue(LDomPropToString(SpellCheckDictionary), Dict); // Set the spell thread: LSpellCheck *t = GetSpellThread(); if (t) Rte->SetSpellCheck(t); } } } else { // Use the built in plain text editor Ctrl = new MailTextView(this, Id, 0, 0, 200, 200, (UseFont) ? &FontType : 0); } } else { // Create a view only control for the mime type: LDocView *HtmlCtrl = NULL; if (!MimeType || !Stricmp(MimeType, sTextPlain) || !Stricmp(MimeType, sMultipartEncrypted)) { Ctrl = new MailTextView(this, Id, 0, 0, 200, 200, (UseFont) ? &FontType : 0); } else { HtmlCtrl = Ctrl = new Html1::LHtml(Id, 0, 0, 200, 200); } if (HtmlCtrl && UseFont) { LVariant LoadImg; if (GetOptions()->GetValue(OPT_HtmlLoadImages, LoadImg)) HtmlCtrl->SetLoadImages(LoadImg.CastInt32() != 0); HtmlCtrl->SetFont(FontType.Create(), true); } } if (Ctrl) { Ctrl->SetUrlDetect(true); Ctrl->SetAutoIndent(true); LVariant WrapOption; if (GetOptions()->GetValue(OPT_WordWrap, WrapOption)) { if (WrapOption.CastInt32()) { LVariant WrapCols = 80; GetOptions()->GetValue(OPT_WrapAtColumn, WrapCols); Ctrl->SetWrapAtCol(WrapCols.CastInt32()); } else { Ctrl->SetWrapAtCol(0); } } } return Ctrl; } void ScribeWnd::GrowlInfo(LString title, LString text) { LGrowl *g = d->GetGrowl(); if (!g) return; LAutoPtr n(new LGrowl::LNotify); n->Name = "info"; n->Title = title; n->Text = text; g->Notify(n); } void ScribeWnd::GrowlOnMail(Mail *m) { LVariant v; LAutoPtr n(new LGrowl::LNotify); n->Name = "new-mail"; n->Title = m->GetSubject(); int Len = 64; char sLen[16]; sprintf_s(sLen, sizeof(sLen), "%i", Len); if (m->GetVariant("BodyAsText", v, sLen)) { char *s = v.Str(); if (s) { int Words = 0; bool Lut[256]; memset(Lut, 0, sizeof(Lut)); Lut[(int)' '] = Lut[(int)'\t'] = Lut[(int)'\r'] = Lut[(int)'\n'] = true; char *c; for (c = s; *c && Words < 30; ) { while (*c && Lut[(int)*c]) c++; while (*c && !Lut[(int)*c]) c++; Words++; } n->Text.Set(s, c - s); } } LGrowl *g = d->GetGrowl(); if (g) { g->Notify(n); m->NewEmail = Mail::NewEmailTray; } } void ScribeWnd::OnNewMailSound() { static uint64 PrevTs = 0; auto Now = LCurrentTime(); if (Now - PrevTs > 30000) { PrevTs = Now; LVariant v; if (GetOptions()->GetValue(OPT_NewMailSoundFile, v) && LFileExists(v.Str())) { LPlaySound(v.Str(), SND_ASYNC); } } } void ScribeWnd::OnFolderSelect(ScribeFolder *f) { if (SearchView) SearchView->OnFolder(); } void ScribeWnd::OnNewMail(List *MailObjs, bool Add) { if (!MailObjs) return; LVariant v; bool ShowDetail = MailObjs->Length() < 5; List NeedsFiltering; LArray NeedsBayes; LArray NeedsGrowl; LArray Resort; for (auto m: *MailObjs) { if (Add) { #if DEBUG_NEW_MAIL LgiTrace("%s:%i - NewMail.OnNewMail t=%p, uid=%s, mode=%s\n", _FL, (Thing*)m, m->GetServerUid().ToString().Get(), toString(m->NewEmail)); #endif switch (m->NewEmail) { case Mail::NewEmailNone: { auto Loaded = m->GetLoaded(); #if DEBUG_NEW_MAIL LgiTrace("%s:%i - NewMail.OnNewMail.GetLoaded=%i uid=%s\n", _FL, (int)Loaded, m->GetServerUid().ToString().Get()); #endif if (Loaded != Store3Loaded) { LOG_STORE("\tOnNewMail calling SetLoaded.\n"); m->SetLoaded(); m->NewEmail = Mail::NewEmailLoading; } else { m->NewEmail = Mail::NewEmailFilter; LOG_STORE("\tOnNewMail none->NeedsFiltering.\n"); NeedsFiltering.Insert(m); } break; } case Mail::NewEmailLoading: { auto Loaded = m->GetLoaded(); if (Loaded == Store3Loaded) { m->NewEmail = Mail::NewEmailFilter; NeedsFiltering.Insert(m); if (m->GetFolder() && !Resort.HasItem(m->GetFolder())) { Resort.Add(m->GetFolder()); } } break; } case Mail::NewEmailFilter: { NeedsFiltering.Insert(m); break; } case Mail::NewEmailBayes: { NeedsBayes.Add(m); break; } case Mail::NewEmailGrowl: { if (d->Growl) { NeedsGrowl.Add(m); break; } else { m->NewEmail = Mail::NewEmailTray; // no Growl loaded so fall through to new tray mail } } case Mail::NewEmailTray: { LAssert(m->GetObject()); Mail::NewMailLst.Insert(m); OnNewMailSound(); break; } default: { LAssert(!"Hmmm what happen?"); break; } } } else { #if DEBUG_NEW_MAIL LgiTrace("%s:%i - NewMail.OnNewMail.RemoveNewMail t=%p, uid=%s\n", _FL, (Thing*)m, m->GetServerUid().ToString().Get()); #endif Mail::NewMailLst.Delete(m); if (m->NewEmail == Mail::NewEmailFilter) m->NewEmail = Mail::NewEmailNone; } } if (Add) { // Do filtering if (NeedsFiltering.Length()) { List Filters; if (!GetOptions()->GetValue(OPT_DisableUserFilters, v) || !v.CastInt32()) { GetFilters(Filters, true, false, false); } if (Filters.Length() > 0) { // Run the filters #if DEBUG_NEW_MAIL LgiTrace("%s:%i - NewMail.OnNewMail.Filtering %i mail through %i filters\n", _FL, (int)NeedsFiltering.Length(), (int)Filters.Length()); #endif Filter::ApplyFilters(NULL, Filters, NeedsFiltering); // All the email not filtered now needs to be sent to the bayes filter. for (auto m: NeedsFiltering) { if (m->NewEmail == Mail::NewEmailBayes) { #if DEBUG_NEW_MAIL LgiTrace("%s:%i - NewMail.OnNewMail.NeedsBayes t=%p, msgid=%s\n", _FL, (Thing*)m, m->GetMessageId()); #endif NeedsBayes.Add(m); } else if (m->NewEmail == Mail::NewEmailGrowl) { #if DEBUG_NEW_MAIL LgiTrace("%s:%i - NewMail.OnNewMail.NeedsGrowl t=%p, msgid=%s\n", _FL, (Thing*)m, m->GetMessageId()); #endif NeedsGrowl.Add(m); } } } } // Do bayes if (NeedsBayes.Length()) { ScribeBayesianFilterMode FilterMode = BayesOff; if (GetOptions()->GetValue(OPT_BayesFilterMode, v)) FilterMode = (ScribeBayesianFilterMode)v.CastInt32(); for (unsigned i=0; iMailMessageIdMap(); // Start the Bayesian rating process off Store3Status Status = IsSpam(Rating, m); if (Status == Store3Success) { // Bayes done... this stops OnBayesResult from passing it back to OnNewMail m->NewEmail = Mail::NewEmailGrowl; // Process bayes result if (!OnBayesResult(m, Rating)) { // Not spam... so on to growl #if DEBUG_NEW_MAIL LgiTrace("%s:%i - NewMail.Bayes.NeedsGrowl t=%p, msgid=%s\n", _FL, (Thing*)m, m->GetMessageId()); #endif NeedsGrowl.Add(m); } else { // Is spam... do nothing... #if DEBUG_NEW_MAIL LgiTrace("%s:%i - NEW_MAIL: Bayes->IsSpam t=%p, msgid=%s\n", _FL, (Thing*)m, m->GetMessageId()); #endif m->NewEmail = Mail::NewEmailNone; m = 0; } } else { // Didn't get classified immediately, so it'll be further // processed when OnBayesResult gets called later. } } else { // Bayes filter not active... move it to growl m->NewEmail = Mail::NewEmailGrowl; NeedsGrowl.Add(m); } } } if (NeedsGrowl.Length()) { if (d->Growl) { if (!ShowDetail) { LAutoPtr n(new LGrowl::LNotify); n->Name = "new-mail"; n->Title = "New Mail"; n->Text.Printf("%i new messages", (int)MailObjs->Length()); d->Growl->Notify(n); } else { for (unsigned i=0; iGetLoaded(); LAssert(state == Store3Loaded); // If loaded then notify GrowlOnMail(m); } } } for (unsigned i=0; iNewEmail = Mail::NewEmailTray; #if DEBUG_NEW_MAIL LgiTrace("%s:%i - NewMail.OnNewMail.Growl->Tray t=%p, msgid=%s\n", _FL, (Thing*)m, m->GetMessageId()); #endif LAssert(m->GetObject()); Mail::NewMailLst.Insert(m); OnNewMailSound(); } } if (GetOptions()->GetValue(OPT_NewMailNotify, v) && v.CastInt32()) { PostEvent(M_SCRIBE_NEW_MAIL); } for (unsigned i=0; iGetPath(); LgiTrace("%s:%i - NewMail.OnNewMail.Resort=%s\n", _FL, Path.Get()); #endif Resort[i]->ReSort(); } } } LColour ScribeWnd::GetColour(int i) { static LColour MailPreview; static LColour UnreadCount; #define ReadColDef(Var, Tag, Default) \ case Tag: \ { \ if (!Var.IsValid()) \ { \ Var = Default; \ LColour::GetConfigColour("Colour."#Tag, Var); \ } \ return Var; \ break; \ } switch (i) { ReadColDef(MailPreview, L_MAIL_PREVIEW, LColour(0, 0, 255)); ReadColDef(UnreadCount, L_UNREAD_COUNT, LColour(0, 0, 255)); default: { return LColour((LSystemColour)i); break; } } return LColour(); } bool WriteXmlTag(LStream &p, LXmlTag *t) { const char *Tag = t->GetTag(); bool ValidTag = ValidStr(Tag) && !IsDigit(Tag[0]); if (ValidTag) p.Print("<%s", Tag); else { LAssert(0); return false; } LXmlTree Tree; static const char *EncodeEntitiesAttr = "\'<>\"\n"; for (unsigned i=0; iAttr.Length(); i++) { auto &a = t->Attr[i]; // Write the attribute name p.Print(" %s=\"", a.GetName()); // Encode the value if (!Tree.EncodeEntities(&p, a.GetValue(), -1, EncodeEntitiesAttr)) { LAssert(0); return false; } // Write the delimiter p.Write((void*)"\"", 1); if (iAttr.Length()-1 /*&& TestFlag(d->Flags, GXT_PRETTY_WHITESPACE)*/) { p.Write((void*)"\n", 1); } } p.Write(">", 1); return true; } LString ScribeWnd::ProcessReplyForwardTemplate(Mail *m, Mail *r, char *Xml, int &Cursor, const char *MimeType) { LStringPipe p(256); if (m && r && Xml) { bool IsHtml = MimeType && !_stricmp(MimeType, sTextHtml); LMemStream mem(Xml, strlen(Xml)); LXmlTag x; LXmlTree t(GXT_KEEP_WHITESPACE | GXT_NO_DOM); if (t.Read(&x, &mem, 0)) { ScribeDom Dom(this); Dom.Email = m; if (IsHtml) { const char *EncodeEntitiesContent = "\'<>\""; for (auto Tag: x.Children) { if (!WriteXmlTag(p, Tag)) { break; } for (const char *c = Tag->GetContent(); c; ) { const char *s = strstr(c, "") : NULL; if (s && e) { if (s > c) { t.EncodeEntities(&p, (char*)c, s - c, EncodeEntitiesContent); } s += 2; LString Var = LString(s, e - s).Strip(); LVariant v; if (Var) { LString::Array parts = Var.SplitDelimit(" "); if (parts.Length() > 0) { if (Dom.GetValue(parts[0], v)) { for (unsigned mod = 1; mod < parts.Length(); mod++) { LString::Array m = parts[mod].SplitDelimit("=", 1); if (m.Length() == 2) { if (m[0].Equals("quote")) { LVariant Quote; if (Dom.GetValue(m[1], Quote)) { LVariant WrapColumn; if (!GetOptions()->GetValue(OPT_WrapAtColumn, WrapColumn) || WrapColumn.CastInt32() <= 0) WrapColumn = 76; WrapAndQuote(p, Quote.Str(), WrapColumn.CastInt32(), v.Str(), NULL, MimeType); v.Empty(); } } } } switch (v.Type) { case GV_STRING: { p.Push(v.Str()); break; } case GV_DATETIME: { p.Push(v.Value.Date->Get()); break; } case GV_NULL: break; default: { LAssert(!"Unsupported type."); break; } } } } } c = e + 2; } else { p.Print("%s", c); break; } } } } else { LArray Tags; Tags.Add(&x); for (auto Tag: x.Children) { Tags.Add(Tag); } for (unsigned i=0; iGetTag() && Dom.GetValue(Tag->GetTag(), v)) { char *s = v.Str(); if (s) { const char *Quote; if ((Quote = Tag->GetAttr("quote"))) { LVariant q, IsQuote; GetOptions()->GetValue(OPT_QuoteReply, IsQuote); if (r->GetValue(Quote, q)) { Quote = q.Str(); } else { Quote = "> "; } if (Quote && IsQuote.CastInt32()) { LVariant WrapColumn; if (!GetOptions()->GetValue(OPT_WrapAtColumn, WrapColumn) || WrapColumn.CastInt32() <= 0) WrapColumn = 76; WrapAndQuote(p, Quote, WrapColumn.CastInt32(), s); } else { p.Push(s); } } else { p.Push(s); } } else if (v.Type == GV_DATETIME && v.Value.Date) { char b[64]; v.Value.Date->Get(b, sizeof(b)); p.Push(b); } } else if (Tag->IsTag("cursor")) { int Size = (int)p.GetSize(); char *Buf = new char[Size+1]; if (Buf) { p.Peek((uchar*)Buf, Size); Buf[Size] = 0; RemoveReturns(Buf); Cursor = LCharLen(Buf, "utf-8"); DeleteArray(Buf); } } if (Tag->GetContent()) { p.Push(Tag->GetContent()); } } } } } return p.NewLStr(); } LAutoString ScribeWnd::ProcessSig(Mail *m, char *Xml, const char *MimeType) { LStringPipe p; if (!m || !Xml) return LAutoString(); if (MimeType && !_stricmp(MimeType, sTextHtml)) p.Write(Xml, strlen(Xml)); else { LMemStream mem(Xml, strlen(Xml)); LXmlTag x; LXmlTree t(GXT_KEEP_WHITESPACE|GXT_NO_DOM); if (t.Read(&x, &mem, 0)) { for (auto Tag: x.Children) { if (Tag->IsTag("random-line")) { char *FileName = 0; if ((FileName = Tag->GetAttr("Filename"))) { LFile f(FileName); if (f) { auto Lines = f.Read().SplitDelimit("\r\n"); char *RandomLine = Lines[LRand((unsigned)Lines.Length())]; if (RandomLine) { p.Push(RandomLine); } } } } else if (Tag->IsTag("random-paragraph")) { char *FileName = 0; if ((FileName = Tag->GetAttr("Filename"))) { char *File = LReadTextFile(FileName); if (File) { List Para; for (char *f=File; f && *f; ) { // skip whitespace while (strchr(" \t\r\n", *f)) f++; if (*f) { char *Start = f; char *n; while ((n = strchr(f, '\n'))) { f = n + 1; if (f[1] == '\n' || (f[1] == '\r' && f[2] == '\n')) { break; } } if (f == Start) f += strlen(f); Para.Insert(NewStr(Start, f-Start)); } } DeleteArray(File); char *RandomPara = Para.ItemAt(LRand((int)Para.Length())); if (RandomPara) { p.Push(RandomPara); } Para.DeleteArrays(); } } } else if (Tag->IsTag("include-file")) { char *FileName = 0; if ((FileName = Tag->GetAttr("filename"))) { char *File = LReadTextFile(FileName); if (File) { p.Push(File); DeleteArray(File); } } } else if (Tag->IsTag("quote-file")) { char *FileName = 0; char *QuoteStr = 0; if ((FileName = Tag->GetAttr("filename")) && (QuoteStr = Tag->GetAttr("Quote"))) { } } else { p.Push(Tag->GetContent()); } } } } return LAutoString(p.NewStr()); } // Get the effective permissions for a resource. // // This method can be used by both sync and async code: // In sync mode, don't supply a callback (ie = NULL) and the return value will be: // Store3Error - no access // Store3Delayed - no access, asking the user for password // Store3Success - allow immediate access // // In async mode, supply a callback and wait for the response. // callback(false) - no access // callback(true) - allow immediate access // in this mode the same return values as sync mode are used. Store3Status ScribeWnd::GetAccessLevel(LViewI *Parent, ScribePerm Required, const char *ResourceName, std::function Callback) { if (Required >= CurrentAuthLevel) { if (Callback) Callback(true); return Store3Success; } if (!Parent) Parent = this; switch (Required) { default: break; case PermRequireUser: { LPassword p; if (!p.Serialize(GetOptions(), OPT_UserPermPassword, false)) { if (Callback) Callback(true); return Store3Success; } char Msg[256]; sprintf_s(Msg, sizeof(Msg), LLoadString(IDS_ASK_USER_PASS), ResourceName); auto d = new LInput(Parent, "", Msg, AppName, true); d->DoModal([this, d, p, Callback](auto dlg, auto id) { if (id && d->GetStr()) { char Pass[256]; p.Get(Pass); bool Status = strcmp(Pass, d->GetStr()) == 0; if (Status) { CurrentAuthLevel = PermRequireUser; auto i = Menu->FindItem(IDM_LOGOUT); if (i) i->Enabled(true); if (Callback) Callback(true); } else { if (Callback) Callback(false); } } delete dlg; }); return Store3Delayed; } case PermRequireAdmin: { LString Key; Key.Printf("Scribe.%s", OPT_AdminPassword); auto Hash = LAppInst->GetConfig(Key); if (ValidStr(Hash)) { if (Callback) Callback(false); return Store3Error; } uchar Bin[256]; ssize_t BinLen = 0; if ((BinLen = ConvertBase64ToBinary(Bin, sizeof(Bin), Hash, strlen(Hash))) != 16) { LgiMsg(Parent, "Admin password not correctly encoded.", AppName); if (Callback) Callback(false); return Store3Error; } auto d = new LInput(Parent, "", LLoadString(IDS_ASK_ADMIN_PASS), AppName, true); d->DoModal([this, d, Bin, Callback](auto dlg, auto id) { if (id && d->GetStr()) { unsigned char Digest[16]; char Str[256]; sprintf_s(Str, sizeof(Str), "%s admin", d->GetStr().Get()); MDStringToDigest(Digest, Str); if (memcmp(Bin, Digest, 16) == 0) { CurrentAuthLevel = PermRequireAdmin; auto i = Menu->FindItem(IDM_LOGOUT); if (i) i->Enabled(true); if (Callback) Callback(true); } else { if (Callback) Callback(false); } } delete dlg; }); return Store3Delayed; } } if (Callback) Callback(true); return Store3Success; } void ScribeWnd::GetAccountSettingsAccess(LViewI *Parent, ScribeAccessType AccessType, std::function Callback) { LVariant Level = (int)PermRequireNone; // Check if user level access is required char *Opt = (char*)(AccessType == ScribeReadAccess ? OPT_AccPermRead : OPT_AccPermWrite); GetOptions()->GetValue(Opt, Level); /* // Check if admin access is required char *Admin = GetScribeAccountPerm((char*) (AccessType == ScribeReadAccess ? "Read" : "Write")); if (Admin && _stricmp(Admin, "Admin") == 0) { Level = PermRequireAdmin; } */ GetAccessLevel(Parent ? Parent : this, (ScribePerm)Level.CastInt32(), "Account Settings", Callback); } LMutex *ScribeWnd::GetLock() { return _Lock; } void ScribeWnd::OnBeforeConnect(ScribeAccount *Account, bool Receive) { if (Receive) { Account->Receive.Enabled(false); } else { Account->Send.Enabled(true); } if (StatusPanel) { StatusPanel->Invalidate(); } } void ScribeWnd::OnAfterConnect(ScribeAccount *Account, bool Receive) { if (Account) { SaveOptions(); if (ScribeState == ScribeExiting) { LCloseApp(); } } if (Receive) { Account->Receive.Enabled(true); } else { Account->Send.Enabled(true); } if (StatusPanel) { StatusPanel->Invalidate(); } if (d->SendAfterReceive) { bool Online = false; for (auto a: Accounts) { bool p = a->Receive.IsPersistant(); bool o = a->Receive.IsOnline(); if (!p && o) { Online = true; break; } } if (!Online) { Send(-1, true); d->SendAfterReceive = false; } } } void ScribeWnd::Send(int Which, bool Quiet) { if (ScribeState == ScribeExiting) return; if (Which < 0) { LVariant v; if (GetOptions()->GetValue(OPT_DefaultSendAccount, v)) Which = v.CastInt32(); } LArray Outboxes; unsigned i; for (i=0; iLoadThings(); Outboxes.Add(OutBox); } } if (Outboxes.Length() < 1) { LgiMsg(this, LLoadString(IDS_NO_OUTGOING_FOLDER), AppName, MB_OK); return; } int MailToSend = 0; List Acc; SendAccountlet *Default = 0; { // Create list of accounts for (auto a: Accounts) { Acc.Insert(&a->Send); a->Send.Outbox.DeleteObjects(); if (Which < 0 || a->GetIndex() == Which) Default = &a->Send; } // If the default it not in the list try the first one.. if (!Default) Default = Acc[0]; } for (i=0; iItems) { Mail *m = t->IsMail(); if (!m) continue; uint32_t Flags = m->GetFlags(); if (!TestFlag(Flags, MAIL_SENT) && TestFlag(Flags, MAIL_READY_TO_SEND)) { LDataIt To = m->GetObject()->GetList(FIELD_TO); if (To && To->Length()) { LAutoPtr Out(new ScribeEnvelope); if (Out) { if (m->OnBeforeSend(Out)) { SendAccountlet *Send = 0; for (auto a: Acc) { LVariant Ie = a->GetAccount()->Identity.Email(); if (ValidStr(Ie.Str()) && a->OnlySendThroughThisAccount()) { if (Ie.Str() && m->GetFromStr(FIELD_EMAIL) && _stricmp(Ie.Str(), m->GetFromStr(FIELD_EMAIL)) == 0) { Send = a; break; } } } if (!Send) { Send = Default; } if (Send) { LAssert(Out->To.Length() > 0); Out->SourceFolder = OutBox->GetPath(); Send->Outbox.Add(Out.Release()); MailToSend++; } } } } else { LgiMsg( this, LLoadString(IDS_ERROR_NO_RECIPIENTS), AppName, MB_OK, m->GetSubject() ? m->GetSubject() : (char*)"(none)"); } } } } if (MailToSend) { for (auto a: Acc) { if (a->Outbox.Length() > 0 && !a->IsOnline()) { if (a->IsConfigured()) { a->Connect(0, Quiet); } else { auto d = new LAlert(this, AppName, LLoadString(IDS_ERROR_NO_CONFIG_SEND), LLoadString(IDS_CONFIGURE), LLoadString(IDS_CANCEL)); d->DoModal([this, d, a](auto dlg, auto id) { if (id == 1) a->GetAccount()->InitUI(this, 1, NULL); delete dlg; }); } } } } else { LgiMsg(this, LLoadString(IDS_NO_MAIL_TO_SEND), AppName, MB_OK, Outboxes[0]->GetText()); } } void ScribeWnd::Receive(int Which) { #define LOG_RECEIVE 0 if (ScribeState == ScribeExiting) { LgiTrace("%s:%i - Won't receive, is trying to exit.\n", _FL); return; } for (ScribeAccount *i: Accounts) { if (i->GetIndex() != Which) continue; if (i->Receive.IsOnline()) { #if LOG_RECEIVE LgiTrace("%s:%i - %i already online.\n", _FL, Which); #endif } else if (i->Receive.Disabled() > 0) { #if LOG_RECEIVE LgiTrace("%s:%i - %i is disabled.\n", _FL, Which); #endif } else if (!i->Receive.IsConfigured()) { #if LOG_RECEIVE LgiTrace("%s:%i - %i is not configured.\n", _FL, Which); #endif auto a = new LAlert(this, AppName, LLoadString(IDS_ERROR_NO_CONFIG_RECEIVE), LLoadString(IDS_CONFIGURE), LLoadString(IDS_CANCEL)); a->DoModal([this, a, i](auto dlg, auto id) { if (id == 1) i->InitUI(this, 2, NULL); delete dlg; }); } else { i->Receive.Connect(0, false); } break; } } bool ScribeWnd::GetHelpFilesPath(char *Path, int PathSize) { const char *Index = "index.html"; char Install[MAX_PATH_LEN]; strcpy_s(Install, sizeof(Install), ScribeResourcePath()); for (int i=0; i<5; i++) { char p[MAX_PATH_LEN]; LMakePath(p, sizeof(p), Install, "help"); LMakePath(p, sizeof(p), p, Index); LgiTrace("Trying '%s'\n", p); if (LFileExists(p)) { LTrimDir(p); strcpy_s(Path, PathSize, p); return true; } #ifdef MAC LMakePath(p, sizeof(p), Install, "Resources/Help"); LMakePath(p, sizeof(p), p, Index); // LgiTrace("Trying '%s'\n", p); if (LFileExists(p)) { LTrimDir(p); strcpy_s(Path, PathSize, p); return true; } #endif LTrimDir(Install); // Try all the parent folders... } LArray Ext; LArray Help; Ext.Add("index.html"); LMakePath(Install, sizeof(Install), ScribeResourcePath(), ".."); LRecursiveFileSearch(Install, &Ext, &Help); for (unsigned i=0; iAddPath(ScribeResourcePath()); Browse->SetEvents(d); return Browse->SetUri(Path); } #else #ifdef MAC if (LExecute(Path)) return true; #else if (Hash) *Hash = '#'; // Get browser... char Browser[256]; if (!LGetAppForMimeType("application/browser", Browser, sizeof(Browser))) { LgiTrace("%s:%i - LGetAppForMimeType('text/html') failed.\n", _FL); goto HelpError; } // Execute browser to view help... char Uri[256]; sprintf_s(Uri, sizeof(Uri), "\"file://%s\"", Path); #ifdef WIN32 char *c; while (c = strchr(Uri, '\\')) *c = '/'; #endif LgiTrace("LaunchHelp('%s','%s').\n", Browser, Uri); if (!LExecute(Browser, Uri)) { LgiTrace("%s:%i - LExecute('%s','%s') failed.\n", _FL, Browser, Uri); goto HelpError; } return true; #endif #endif HelpError: LgiMsg(this, LLoadString(IDS_ERROR_NO_HELP), AppName, MB_OK); } return false; } void ScribeWnd::Preview(int Which) { LArray a; a.Add(Accounts[Which]); OpenPopView(this, a); } bool MergeSegments(LDataPropI *DstProp, LDataPropI *SrcProp, LDom *Dom) { LDataI *Src = dynamic_cast(SrcProp); LDataI *Dst = dynamic_cast(DstProp); if (!Dst || !Src) return false; Store3MimeType Mt(Src->GetStr(FIELD_MIME_TYPE)); if (Mt.IsText()) { // Set the headers... Dst->SetStr(FIELD_INTERNET_HEADER, Src->GetStr(FIELD_INTERNET_HEADER)); // Do mail merge of data part... LAutoStreamI Data = Src->GetStream(_FL); if (Data) { // Read data out into a string string... LAutoString Str(new char[(int)Data->GetSize()+1]); Data->Read(Str, (int)Data->GetSize()); Str[Data->GetSize()] = 0; // Do field insert and save result to segment char *Merged = ScribeInsertFields(Str, Dom); LAutoStreamI Mem(new LMemStream(Merged, strlen(Merged))); Dst->SetStream(Mem); } } else { // Straight copy... Dst->CopyProps(*Src); } // Merge children segments as well LDataIt Sc = Src->GetList(FIELD_MIME_SEG); LDataIt Dc = Dst->GetList(FIELD_MIME_SEG); if (Dc && Sc) { for (unsigned i=0; iLength(); i++) { // Create new dest child, and merge the source child across LDataPropI *NewDestChild = Dc->Create(Dst->GetStore()); if (!MergeSegments(NewDestChild, (*Sc)[i], Dom)) return false; LDataI *NewDest = dynamic_cast(NewDestChild); if (NewDest) NewDest->Save(Dst); } } return true; } // Either 'FileName' or 'Source' will be valid void ScribeWnd::MailMerge(LArray &Contacts, const char *FileName, Mail *Source) { ScribeFolder *Outbox = GetFolder(FOLDER_OUTBOX); if (Outbox && Contacts.Length() && (FileName || Source)) { LAutoPtr ImportEmail; if (FileName) { LAutoPtr File(new LFile); if (File->Open(FileName, O_READ)) { Thing *t = CreateItem(MAGIC_MAIL, Outbox, false); ImportEmail.Reset(Source = t->IsMail()); if (!Source->Import(Source->AutoCast(File), sMimeMessage)) { Source = 0; } } } if (Source) { List Msgs; ScribeDom Dom(this); // Do the merging of the document with the database for (unsigned i=0; iGetContact() : 0; Contact *temp = new Contact(this); if (!c) { temp->SetFirst(la->sName); temp->SetEmail(la->sAddr); c = temp; } Dom.Con = c; Thing *t = CreateItem(MAGIC_MAIL, Outbox, false); if (t) { Dom.Email = t->IsMail(); LAutoString s; if (s.Reset(ScribeInsertFields(Source->GetSubject(), &Dom))) Dom.Email->SetSubject(s); LDataPropI *Recip = Dom.Email->GetTo()->Create(Dom.Email->GetObject()->GetStore()); if (Recip) { Recip->CopyProps(*Dom.Con->GetObject()); Recip->SetInt(FIELD_CC, 0); Dom.Email->GetTo()->Insert(Recip); } MergeSegments( Dom.Email->GetObject()->GetObj(FIELD_MIME_SEG), Source->GetObject()->GetObj(FIELD_MIME_SEG), &Dom); Msgs.Insert(Dom.Email); } temp->DecRef(); } // Ask user what to do if (Msgs[0]) { char Msg[256]; sprintf_s(Msg, sizeof(Msg), LLoadString(IDS_MAIL_MERGE_Q), Msgs.Length()); auto Ask = new LAlert(this, AppName, Msg, LLoadString(IDS_SAVE_TO_OUTBOX), LLoadString(IDS_CANCEL)); Ask->DoModal([this, Msgs, Outbox](auto dlg, auto id) { switch (id) { case 1: // Save To Outbox { for (size_t i=0; iSave(Outbox); } break; } case 2: // Cancel { for (size_t i=0; iOnDelete(); } break; } } delete dlg; }); } else { LgiMsg(this, LLoadString(IDS_MAIL_MERGE_EMPTY), AppName); } } } } ScribeFolder *CastFolder(LDataI *f) { if (!f) { LAssert(!"Null pointer"); return 0; } if (f->Type() == MAGIC_FOLDER) { return (ScribeFolder*)f->UserData; } return 0; } Thing *CastThing(LDataI *t) { if (!t) { LAssert(!"Null pointer"); return 0; } if (t->Type() == MAGIC_FOLDER) { LAssert(!"Shouldn't be a folder"); return 0; } return (Thing*)t->UserData; } LString _GetUids(LArray &items) { LString::Array a; for (auto i: items) a.New().Printf(LPrintfInt64, i->GetInt(FIELD_SERVER_UID)); return LString(",").Join(a); } /// Received new items from a storage backend. void ScribeWnd::OnNew( /// The parent folder of the new item LDataFolderI *Parent, /// All the new items LArray &NewItems, /// The position in the parent folder or -1 int Pos, /// Non-zero if the object is a new email. bool IsNew) { ScribeFolder *Fld = CastFolder(Parent); int UnreadDiff = 0; if (Stricmp(Parent->GetStr(FIELD_FOLDER_NAME), "Contacts") && Stricmp(Parent->GetStr(FIELD_FOLDER_NAME), "Calendar")) { LOG_STORE("OnNew(%s, %s, %i, %i)\n", Parent->GetStr(FIELD_FOLDER_NAME), _GetUids(NewItems).Get(), Pos, IsNew); } if (!Fld) { // When this happens the parent hasn't been loaded by the UI thread // yet, so if say the IMAP back end notices a new sub-folder then we // can safely ignore it until such time as the UI loads the parent // folder. At which point the child we got told about here will be // loaded anyway. #if DEBUG_NEW_MAIL LDataFolderI *p = dynamic_cast(Parent); LgiTrace("%s:%i - NewMail.OnNew no UI object for '%s'\n", _FL, p ? p->GetStr(FIELD_FOLDER_NAME) : NULL); #endif return; } if (!Parent || !NewItems.Length()) { LAssert(!"Param error"); return; } for (auto cb: d->Store3EventCallbacks) cb->OnNew(Parent, NewItems, Pos, IsNew); List NewMail; for (auto Item: NewItems) { if (Item->Type() == MAGIC_FOLDER) { // Insert new folder into the right point on the tree... LDataFolderI *SubObject = dynamic_cast(Item); if (!SubObject) { LAssert(!"Not a valid folder."); continue; } ScribeFolder *Sub = CastFolder(SubObject); // LgiTrace("OnNew '%s', Sub=%p Children=%i\n", SubObject->GetStr(FIELD_FOLDER_NAME), Sub, SubObject->SubFolders().Length()); if (!Sub) { // New folder... if ((Sub = new ScribeFolder)) { Sub->App = this; Sub->SetObject(SubObject, false, _FL); Fld->Insert(Sub); } } else { // Existing folder... Fld->Insert(Sub, Pos); } } else { Thing *t = CastThing(Item); if (!t) { // Completely new thing... t = CreateThingOfType((Store3ItemTypes) Item->Type(), Item); } if (t) { if (t->DeleteOnAdd.Obj) { // Complete a delayed move auto OldFolder = t->App->GetFolder(t->DeleteOnAdd.Path); if (!OldFolder) { LgiTrace("%s:%i - Couldn't resolve old folder '%s'\n", _FL, t->DeleteOnAdd.Path.Get()); } else { auto OldItem = t->DeleteOnAdd.Obj; if (!OldFolder->Items.HasItem(OldItem)) LgiTrace("%s:%i - Couldn't find old obj.\n", _FL); else OldItem->OnDelete(); } } t->SetParentFolder(Fld); Fld->Update(); if (Fld->Select()) { // Existing thing... t->SetFieldArray(Fld->GetFieldArray()); ThingFilter *Filter = GetThingFilter(); if (!Filter || Filter->TestThing(t)) { MailList->Insert(t, -1, false); } } Mail *m = t->IsMail(); if (m) { // LgiTrace("OnNew %p\n", m->GetObject()); UnreadDiff += TestFlag(m->GetFlags(), MAIL_READ) ? 0 : 1; #if DEBUG_NEW_MAIL LgiTrace("%s:%i - NewMail.OnNew t=%p uid=%s IsNew=%i\n", _FL, t, m->GetServerUid().ToString().Get(), IsNew); #endif if (IsNew) { LAssert(!NewMail.HasItem(m)); NewMail.Insert(m); } } t->Update(); } } } if (UnreadDiff) Fld->OnUpdateUnRead(UnreadDiff, false); if (MailList && Fld->Select()) MailList->Sort(ListItemCompare, (NativeInt)Fld); if (NewMail.Length()) OnNewMail(&NewMail); } void ScribeWnd::OnPropChange(LDataStoreI *store, int Prop, LVariantType Type) { switch (Prop) { case FIELD_IS_ONLINE: { // This is in case we receive a message after the app has shutdown and // deleted 'this'. if (ScribeState > ScribeRunning) break; if (StatusPanel) StatusPanel->Invalidate(); for (auto a : Accounts) { if (a->Receive.GetDataStore() == store) { int64 Online = store->GetInt(FIELD_IS_ONLINE); auto Old = ScribeState; // This prevents the folders unloading during this call. // Which causes crashes. ScribeState = ScribeLoadingFolders; a->Receive.OnOnlineChange(Online != 0); ScribeState = Old; break; } } break; } } } void ScribeWnd::SetContext(const char *file, int line) { d->CtxFile = file; d->CtxLine = line; } bool ScribeWnd::OnChange(LArray &items, int FieldHint) { bool UpdateSelection = false; List NewMail; ScribeFolder *Parent = 0; // LOG_STORE("OnChange(%i, %i)\n", (int)items.Length(), FieldHint); LAssert(d->CtxFile != NULL); for (unsigned c=0; cStore3EventCallbacks.Length(); c++) { d->Store3EventCallbacks[c]->OnChange(items, FieldHint); } for (unsigned i=0; iType() == MAGIC_FOLDER) { auto ItemFolder = dynamic_cast(Item); ScribeFolder *fld = CastFolder(ItemFolder); if (fld) { if (FieldHint == FIELD_STATUS) { // This is the delayed folder load case: fld->IsLoaded(true); if (fld->Select()) fld->Populate(GetMailList()); } else { fld->Update(); } } else { // LAssert(!"Can't cast to folder?"); } } else if ((t = CastThing(Item))) { ThingUi *Ui = t->GetUI(); if (Ui) Ui->OnChange(); Mail *m = t->IsMail(); if (m) { auto StoreFlags = Item->GetInt(FIELD_FLAGS); #if 0 LgiTrace("App.OnChange(%i) handler %p: %s -> %s\n", FieldHint, m, EmailFlagsToStr(m->FlagsCache).Get(), EmailFlagsToStr(StoreFlags).Get()); #endif if (TestFlag(m->FlagsCache, MAIL_NEW) && !TestFlag(StoreFlags, MAIL_NEW)) { Mail::NewMailLst.Delete(m); Parent = m->GetFolder(); } if (m->FlagsCache != StoreFlags) { // LgiTrace("%s:%i - OnChange mail flags changed.\n", _FL); m->SetFlagsCache(StoreFlags, false, false); Parent = m->GetFolder(); } if (m->NewEmail == Mail::NewEmailLoading) { auto Loaded = m->GetLoaded(); if (Loaded < Store3Loaded) { #if DEBUG_NEW_MAIL LgiTrace("%s:%i - NewMail.OnChange.GetBody t=%p, uid=%s, mode=%s, loaded=%s (%s:%i)\n", _FL, (Thing*)m, m->GetServerUid().ToString().Get(), toString(m->NewEmail), toString(Loaded), d->CtxFile, d->CtxLine); #endif m->GetBody(); } else { #if DEBUG_NEW_MAIL LgiTrace("%s:%i - NewMail.OnChange.NewMail t=%p, uid=%s, mode=%s, loaded=%s (%s:%i)\n", _FL, (Thing*)m, m->GetServerUid().ToString().Get(), toString(m->NewEmail), toString(Loaded), d->CtxFile, d->CtxLine); #endif NewMail.Insert(m); } } } else if (t->IsCalendar()) { for (auto cv: CalendarView::CalendarViews) cv->OnContentsChanged(); } if (FieldHint != FIELD_FLAGS) { // Call the on load handler... t->IsLoaded(true); } if (t->GetList()) { t->Update(); if (FieldHint != FIELD_FLAGS) UpdateSelection |= t->Select(); } } } if (MailList && UpdateSelection) { List Sel; if (MailList->GetSelection(Sel)) { OnSelect(&Sel, true); } } if (Parent) Parent->OnUpdateUnRead(0, true); if (NewMail.Length()) OnNewMail(&NewMail); d->CtxFile = NULL; d->CtxLine = 0; return true; } ContactGroup *ScribeWnd::FindGroup(char *SearchName) { ScribeFolder *g = GetFolder(FOLDER_GROUPS); if (!g || !SearchName) return 0; g->LoadThings(); for (Thing *t: g->Items) { ContactGroup *Grp = t->IsGroup(); if (!Grp) continue; auto Name = Grp->GetObject()->GetStr(FIELD_GROUP_NAME); if (Name) { if (!_stricmp(Name, SearchName)) { return Grp; } } } return 0; } bool ScribeWnd::Match(LDataStoreI *Store, LDataPropI *Address, int ObjType, LArray &Matches) { if (!Store || !Address) return 0; // char *Addr = Address->GetStr(ObjType == MAGIC_CONTACT ? FIELD_EMAIL : FIELD_NAME); auto Addr = Address->GetStr(FIELD_EMAIL); if (!Addr) return 0; List *l = GetEveryone(); if (!l) return 0; Matches.Length(0); if (ObjType == MAGIC_CONTACT) { for (auto c: *l) { if (strchr(Addr, '@')) { auto Emails = c->GetEmails(); for (auto e: Emails) { if (e.Equals(Addr)) { Matches.Add(c); break; } } } } } else if (ObjType == MAGIC_GROUP) { ScribeFolder *g = GetFolder(FOLDER_GROUPS); if (!g) return false; g->LoadThings(); for (auto t: g->Items) { ContactGroup *Grp = t->IsGroup(); if (!Grp) continue; List GrpAddr; if (!Grp->GetAddresses(GrpAddr)) continue; for (auto a: GrpAddr) { if (_stricmp(a, Addr) == 0) { Matches.Add(t); break; } } GrpAddr.DeleteArrays(); } } return Matches.Length() > 0; } bool ScribeWnd::OnMove(LDataFolderI *new_parent, LDataFolderI *old_parent, LArray &items) { if (!new_parent || items.Length() == 0) return false; bool Status = false; for (auto cb: d->Store3EventCallbacks) cb->OnMove(new_parent, old_parent, items); ScribeFolder *New = CastFolder(new_parent); ScribeFolder *Old = old_parent ? CastFolder(old_parent) : NULL; if (New) { ssize_t SelIdx = -1; for (unsigned n=0; nType()) { case MAGIC_FOLDER: { ScribeFolder *i = CastFolder(items[n]); if (i) { i->Detach(); New->Insert(i); Status = true; } break; } default: { Thing *t = CastThing(items[n]); if (t) { int UnreadMail = t->IsMail() ? !TestFlag(t->IsMail()->GetFlags(), MAIL_READ) : false; if (Old && UnreadMail) Old->OnUpdateUnRead(-1, false); if (t->GetList()) { if (t->Select() && SelIdx < 0) SelIdx = t->GetList()->IndexOf(t); t->GetList()->Remove(t); } // This closes the user interface if the object is being moved to the trash... if (New->GetSystemFolderType() == Store3SystemTrash) t->SetUI(); t->SetParentFolder(New); if (New->Select()) { MailList->Insert(t); MailList->ReSort(); } if (UnreadMail) New->OnUpdateUnRead(1, false); if (New->GetItemType() == MAGIC_ANY && t->IsMail()) { Mail::NewMailLst.Delete(t->IsMail()); } Status = true; } break; } } } if (MailList && SelIdx >= 0 && MailList->Length() > 0) { if (SelIdx >= (ssize_t)MailList->Length()) SelIdx = MailList->Length()-1; MailList->Value(SelIdx); } } return Status; } bool ScribeWnd::OnDelete(LDataFolderI *Parent, LArray &Items) { int UnreadAdjust = 0; int SelectIdx = -1; LOG_STORE("OnDelete(%s, %i)\n", Parent->GetStr(FIELD_FOLDER_NAME), (int)Items.Length()); if (!Items.Length()) return false; for (unsigned c=0; cStore3EventCallbacks.Length(); c++) { d->Store3EventCallbacks[c]->OnDelete(Parent, Items); } ScribeFolder *Fld = NULL; for (unsigned i=0; iType() == MAGIC_FOLDER) { // Insert new folder into the right point on the tree... ScribeFolder *Sub = CastFolder(Item); if (!Sub) return true; LgiTrace("OnDelete folder %p (%i)\n", (ThingType*)Sub, ThingType::DirtyThings.HasItem(Sub)); // Remove the deleted object from the dirty queue... ThingType::DirtyThings.Delete(Sub); // And make sure it can't get dirty again... Sub->SetWillDirty(false); // Remove all the children LDataIterator &SubItems = Sub->GetFldObj()->Children(); LArray Children; for (LDataI *c = SubItems.First(); c; c = SubItems.Next()) { Children.Add(c); } OnDelete(Sub->GetFldObj(), Children); // Remove the UI element Sub->Remove(); // Free the memory for the object DeleteObj(Sub); } else { Fld = Parent ? CastFolder(Parent) : 0; if (!Fld) return false; Thing *t = CastThing(Item); if (!t) { // This happens when the item moves from one store to another // of a different type and a copy of the old object is made. The // 'Thing' is detached from 'Item' and attached to the new LDataI // object. // However if the object is an unread email... we should still decrement the // folder's unread count. if (Item->Type() == MAGIC_MAIL && !(Item->GetInt(FIELD_FLAGS) & MAIL_READ)) { Fld->OnUpdateUnRead(-1, false); } } else { LAssert(!Fld || Fld == t->GetFolder()); // LgiTrace("OnDelete thing %p (%i)\n", (ThingType*)t, ThingType::DirtyThings.HasItem(t)); // Remove the deleted object from the dirty queue... ThingType::DirtyThings.Delete(t); // And make sure it can't get dirty again... t->SetWillDirty(false); // Was the thing currently being previewed? if (PreviewPanel && PreviewPanel->GetCurrent() == t) PreviewPanel->OnThing(0, false); // Was the object selected in the thing list? if (Fld && Fld->Select()) { // If so, select an adjacent item. if (SelectIdx < 0 && t->Select()) SelectIdx = MailList->IndexOf(t); MailList->Remove(t); } // Was the object an unread email? Mail *m = t->IsMail(); if (m) { if (!TestFlag(m->GetFlags(), MAIL_READ) && Fld) UnreadAdjust--; Mail::NewMailLst.Delete(m); } t->SetUI(); t->SetParentFolder(NULL); t->SetObject(NULL, false, _FL); if (Fld) Fld->Update(); if (!t->DecRef()) { t->SetDirty(false); t->SetWillDirty(false); } } } } if (Fld) Fld->OnUpdateUnRead(UnreadAdjust, false); if (SelectIdx >= 0) { LListItem *i = MailList->ItemAt(SelectIdx); if (!i && MailList->Length() > 0) i = MailList->ItemAt(MailList->Length()-1); if (i) i->Select(true); } return true; } bool ScribeWnd::AddStore3EventHandler(LDataEventsI *callback) { if (!d->Store3EventCallbacks.HasItem(callback)) { d->Store3EventCallbacks.Add(callback); } return true; } bool ScribeWnd::RemoveStore3EventHandler(LDataEventsI *callback) { d->Store3EventCallbacks.Delete(callback); return true; } bool ScribeWnd::OnMailTransferEvent(MailTransferEvent *t) { if (!Lock(_FL)) return false; LAssert(t); d->Transfers.Add(t); Unlock(); return true; } bool ScribeWnd::OnTransfer() { LVariant v; LArray Local; // Lock the transfer list if (Lock(_FL)) { // Take out a bunch of emails... for (int i=0; i<5 && d->Transfers.Length() > 0; i++) { auto t = d->Transfers[0]; LAssert(t); d->Transfers.DeleteAt(0, true); // Save them to a local array Local.Add(t); } Unlock(); if (Local.Length() == 0) // Didn't get any return false; } LArray Trans; for (auto &f: Folders) { if (f.Store) Trans.New() = f.Store->StartTransaction(); } for (auto Transfer: Local) { ReceiveStatus NewStatus = MailReceivedError; if (!Transfer) { LAssert(0); continue; } // We have to set Transfer->Status to something other than "waiting" // in all branches of this loop. Otherwise the account thread will hang. Accountlet *Acc = Transfer->Account; #if DEBUG_NEW_MAIL LgiTrace( "%s:%i - NewMail.OnTransfer t=%p receive=%i act=%i\n", _FL, Transfer, Acc->IsReceive(), Transfer->Action); #endif if (Acc->IsReceive()) { switch (Transfer->Action) { default: break; case MailDownload: case MailDownloadAndDelete: { // Import newly received mail from file ReceiveAccountlet *Receive = dynamic_cast(Acc); if (Receive) { LVariant Path = Receive->DestinationFolder(); ScribeFolder *Inbox = Path.Str() ? GetFolder(Path.Str()) : NULL; if (!Inbox) Inbox = GetFolder(FOLDER_INBOX); if (Inbox) { Mail *m = new Mail(this, Inbox->GetObject()->GetStore()->Create(MAGIC_MAIL)); if (m) { // Just in case m->SetParentFolder(Inbox); // Set the new flag... m->SetFlags(m->GetFlags() | MAIL_NEW, true, false); // Set the account to, which is used to map the email back // to the incoming account in the case that the headers // don't have the correct "To" information. m->SetAccountId(Receive->Id()); // Decode the email m->OnAfterReceive(Transfer->Rfc822Msg); LVariant v; m->SetServerUid(v = Transfer->Uid); // Save to permanent storage if (m->Save(Inbox)) { m->SetDirty(false); NewStatus = MailReceivedOk; // Only after we've safely stored the email can we // actually mark it as downloaded. if (Transfer->Uid) { Receive->AddMsg(Transfer->Uid); } } else LgiTrace("%s:%i - Error: Couldn't save mail to folders.\n", _FL); } else LgiTrace("%s:%i - Error: Memory alloc failed.\n", _FL); } else LgiTrace("%s:%i - Error: No Inbox.\n", _FL); } else LgiTrace("%s:%i - Error: Bad ptr.\n", _FL); break; } case MailHeaders: { LList *Lst; if (Transfer->Msg && (Lst = Transfer->GetList())) { //LgiTrace("Using Lst=%p\n", Lst); Lst->Insert(Transfer->Msg); NewStatus = MailReceivedOk; } break; } } } else if (Transfer->Send) { ScribeFolder *Outbox = GetFolder(Transfer->Send->SourceFolder); if (!Outbox) Outbox = GetFolder(FOLDER_OUTBOX); if (!Outbox) break; Outbox->GetMessageById(Transfer->Send->MsgId, [this, Transfer](auto m) { if (!m) { LAssert(!"Where is the email?"); LgiTrace("%s:%i - Can't find outbox for msg id '%s'\n", _FL, Transfer->Send->MsgId.Get()); } else { if (Transfer->OutgoingHeaders) { m->SetInternetHeader(Transfer->OutgoingHeaders); DeleteArray(Transfer->OutgoingHeaders); } m->OnAfterSend(); m->Save(); // Do filtering LVariant DisableFilters; GetOptions()->GetValue(OPT_DisableUserFilters, DisableFilters); if (!DisableFilters.CastInt32()) { // Run the filters List Filters; GetFilters(Filters, false, true, false); if (Filters[0]) { List In; In.Insert(m); Filter::ApplyFilters(0, Filters, In); } } // Add to bayesian spam whitelist... LVariant v; ScribeBayesianFilterMode FilterMode = BayesOff; GetOptions()->GetValue(OPT_BayesFilterMode, v); FilterMode = (ScribeBayesianFilterMode) v.CastInt32(); if (FilterMode != BayesOff && m->GetObject()) { LDataIt To = m->GetObject()->GetList(FIELD_TO); if (To) { for (LDataPropI *a = To->First(); a; a = To->Next()) { if (a->GetStr(FIELD_EMAIL)) WhiteListIncrement(a->GetStr(FIELD_EMAIL)); } } } // FIXME: // NewStatus = MailReceivedOk; } }); } #if DEBUG_NEW_MAIL LgiTrace( "%s:%i - NewMail.OnTransfer t=%p NewStatus=%i\n", _FL, Transfer, NewStatus); #endif if (NewStatus != MailReceivedOk) { // So tell the thread not to delete it from the server LgiTrace("%s:%i - Mail[%i] error: %s\n", _FL, Transfer->Index, ReceiveStatusName(Transfer->Status)); Transfer->Action = MailNoop; } Transfer->Status = NewStatus; } return Local.Length() > 0; } bool ScribeWnd::OnIdle() { bool Status = false; for (auto a : Accounts) Status |= a->Receive.OnIdle(); Status |= OnTransfer(); LMessage m(M_SCRIBE_IDLE); BayesianFilter::OnEvent(&m); SaveDirtyObjects(); #ifdef _DEBUG static uint64_t LastTs = 0; auto Now = LCurrentTime(); if (Now - LastTs >= 1000) { LastTs = Now; if (Thing::DirtyThings.Length() > 0) LgiTrace("%s:%i - Thing::DirtyThings=" LPrintfInt64 "\n", _FL, Thing::DirtyThings.Length()); } #endif return Status; } void ScribeWnd::OnScriptCompileError(const char *Source, Filter *f) { const char *CompileMsg = LLoadString(IDS_ERROR_SCRIPT_COMPILE); LArray Actions; Actions.Add(LLoadString(IDS_SHOW_CONSOLE)); Actions.Add(LLoadString(IDS_OPEN_SOURCE)); Actions.Add(LLoadString(IDS_OK)); if (!d->Bar) { // FYI Capabilities are handled in ScribeWnd::StartAction. d->ErrSource = Source; d->ErrFilter = f; d->Bar = new MissingCapsBar(this, &d->MissingCaps, CompileMsg, this, Actions); AddView(d->Bar, 2); AttachChildren(); OnPosChange(); } } #ifdef _DEBUG #include "Store3Mail3/Mail3.h" class NoSaveOptions : public LOptionsFile { bool Serialize(bool Write) { return true; } public: NoSaveOptions(LOptionsFile *opts) : LOptionsFile(PortableMode, AppName) { } }; struct ScribeUnitTest { uint64_t StartTs = 0; int Timeout = 5000; virtual ~ScribeUnitTest() {} virtual void OnTimeout() {} virtual void Run(std::function Callback) = 0; virtual void OnPulse() { if (StartTs != 0 && LCurrentTime() - StartTs >= Timeout) { LgiTrace("%s:%i - UnitTest timed out.\n", _FL); StartTs = 0; OnTimeout(); } } }; struct UnitTestState : public LView::ViewEventTarget, public LThread, public LCancel { ScribeWnd *App = NULL; NoSaveOptions *Opts = NULL; std::function Callback; LArray Tests; LAutoPtr t; bool Status = true; bool IsFinished = false; // Old app state.. LAutoPtr OldOpts; LArray OldFolders; UnitTestState(ScribeWnd *app, std::function callback); ~UnitTestState(); LMessage::Result OnEvent(LMessage *Msg) override; bool Iterate(); bool Done(); int Main() override; LDataStoreI *CreateTestMail3(); // Wrappers for protected elements: LArray &GetFolders() { return App->Folders; } void UnLoadFolders() { App->UnLoadFolders(); } }; #define UnitTestFail() { if (Callback) Callback(false); return; } #define UnitTestPass() { if (Callback) Callback(true); return; } // Basic load mail3 folder... struct LoadMailStore1 : public ScribeUnitTest { UnitTestState *s; LString folderPath; bool GotCallback = false; std::function Callback; LoadMailStore1(UnitTestState *state) : s(state) { } ~LoadMailStore1() { if (folderPath) { s->UnLoadFolders(); LFile::Path p = folderPath; p--; FileDev->RemoveFolder(p, true); } LAssert(GotCallback); } // Do operations on mail store before we try and load it... virtual void OnMailStore(LDataStoreI *store) {} virtual void ConfigureLoadMailStoreState(LoadMailStoreState *state) {} void OnTimeout() { GotCallback = true; Callback(false); // This should delete 'this' } void Run(std::function cb) { Callback = cb; if (auto store = s->CreateTestMail3()) { folderPath = store->GetStr(FIELD_NAME); OnMailStore(store); delete store; } // Setup the options saying what folders to load... s->Opts->CreateTag(OPT_MailStores); auto MailStores = s->Opts->LockTag(OPT_MailStores, _FL); if (MailStores == NULL) UnitTestFail(); auto ms = MailStores->CreateTag(OPT_MailStore); ms->SetAttr(OPT_MailStoreLocation, folderPath); ms->SetAttr(OPT_MailStoreDisable, "0"); ms->SetAttr(OPT_MailStoreName, "testing"); s->Opts->Unlock(); if (!ms) UnitTestFail(); // Try the load... if (auto state = new LoadMailStoreState(s->App, [this](auto status) { GotCallback = true; auto &Folders = s->GetFolders(); if (Folders.Length() == 0) UnitTestFail() bool found = false; for (auto &f: Folders) { if (f.Path == folderPath && f.Store != NULL) { found = true; break; } } if (!found) UnitTestFail(); Callback(status); }) ) { ConfigureLoadMailStoreState(state); state->Start(); } } }; // This tests the DB schema upgrade functionality of the mail3 store. struct LoadMailStore2 : public LoadMailStore1 { LoadMailStore2(UnitTestState *state) : LoadMailStore1(state) { } void OnMailStore(LDataStoreI *store) override { auto ms3 = dynamic_cast(store); if (!ms3) UnitTestFail(); // Delete a field from the database so that we force an "upgrade" LMail3Store::LStatement s(ms3, "alter table Calendar rename column DateModified to DateMod"); if (!s.Exec()) UnitTestFail(); } // This is a simple placeholder dialog to simulate asking the user to // upgrade the mail store. It then returns a 'Yes' to the callback after // a few seconds. struct UpgradeYes : public LWindow { UnitTestState *s; LoadMailStoreState::IntCb Cb; UpgradeYes(UnitTestState *state, LoadMailStoreState::IntCb cb) : s(state), Cb(cb) { Name("Upgrade Dlg"); // Just needs to be open long enough to run the msg loop a bit. SetPulse(500); LRect r(0, 0, 100, 100); SetPos(r); MoveSameScreen(s->App); if (Attach(NULL)) Visible(true); } void OnPulse() { Cb(IDYES); Quit(); } }; void ConfigureLoadMailStoreState(LoadMailStoreState *state) override { state->AskStoreUpgrade = [this](auto path, auto detail, auto cb) { new UpgradeYes(s, cb); }; } }; UnitTestState::UnitTestState(ScribeWnd *app, std::function callback) : LView::ViewEventTarget(app, M_UNIT_TEST_TICK), LThread("UnitTestState"), App(app), Callback(callback) { OldOpts = App->d->Options; OldFolders.Swap(App->Folders); App->d->Options.Reset(Opts = new NoSaveOptions(OldOpts)); Tests.Add(new LoadMailStore1(this)); Tests.Add(new LoadMailStore2(this)); LgiTrace("%s:%i - Starting with " LPrintfInt64 " unit tests.\n", _FL, Tests.Length()); Run(); } UnitTestState::~UnitTestState() { Cancel(); // Restore app state... App->d->Options = OldOpts; OldFolders.Swap(App->Folders); WaitForExit(); } LMessage::Result UnitTestState::OnEvent(LMessage *Msg) { if (!IsFinished && Msg->Msg() == M_UNIT_TEST_TICK) { if (t) t->OnPulse(); else Iterate(); } return 0; } bool UnitTestState::Iterate() { if (Tests.Length() == 0) return Done(); t.Reset(Tests[0]); Tests.DeleteAt(0, true); t->StartTs = LCurrentTime(); LgiTrace("%s:%i - Running unit test..\n", _FL); t->Run([this](auto ok) { LgiTrace("%s:%i - Unit test status: %i\n", _FL, ok); Status &= ok; t.Reset(); // Reset for the next unit test. }); return true; } bool UnitTestState::Done() { // Stop more tick events.. IsFinished = true; Cancel(); if (Callback) Callback(Status); delete this; return true; } int UnitTestState::Main() { while (!IsCancelled()) { PostEvent(M_UNIT_TEST_TICK); LSleep(500); } return 0; } LDataStoreI *UnitTestState::CreateTestMail3() { LFile::Path p(ScribeTempPath()); p += "UnitTesting"; if (!p.Exists()) FileDev->CreateFolder(p); p += "Folders.mail3"; return App->CreateDataStore(p, true); } void ScribeWnd::UnitTests(std::function Callback) { UnLoadFolders(); new UnitTestState(this, Callback); } #endif diff --git a/src/ScribeAttachment.cpp b/src/ScribeAttachment.cpp --- a/src/ScribeAttachment.cpp +++ b/src/ScribeAttachment.cpp @@ -1,1187 +1,1186 @@ /* ** FILE: ScribeAttachment.cpp ** AUTHOR: Matthew Allen ** DATE: 7/12/98 ** DESCRIPTION: Scribe Attachments ** ** Copyright (C) 1998, Matthew Allen ** fret@memecode.com */ #include #include #include #include #include "Scribe.h" #include "resdefs.h" -#include "lgi/common/NetTools.h" #include "lgi/common/ProgressDlg.h" #include "lgi/common/Tnef.h" #include "lgi/common/LgiRes.h" #include "lgi/common/TextConvert.h" #include "lgi/common/FileSelect.h" extern char *ExtractCodePage(char *ContentType); #ifdef WIN32 char NotAllowed[] = "\\/*:?\"|<>"; #else char NotAllowed[] = "\\/"; #endif ////////////////////////////////////////////////////////////////////////////// char *StripPath(const char *Full) { if (Full) { auto Dos = strrchr(Full, '\\'); auto Unix = strrchr(Full, '/'); if (Dos) { return NewStr(Dos+1); } else if (Unix) { return NewStr(Unix+1); } } return NewStr(Full); } void CleanFileName(char *i) { if (i) { char *o = i; while (*i) { if ((uint8_t)*i >= ' ' && !strchr(NotAllowed, *i)) { *o++ = *i; } i++; } *o++ = 0; } } ////////////////////////////////////////////////////////////////////////////// void Attachment::_New(LDataI *object) { DefaultObject(object); LAssert(GetObject() != NULL); Owner = 0; Msg = 0; IsResizing = false; } Attachment::Attachment(ScribeWnd *App, Attachment *From) : Thing(App) { _New(From && From->GetObject() ? From->GetObject()->GetStore()->Create(MAGIC_ATTACHMENT) : 0); if (From) { char *Ptr = 0; ssize_t Len = 0; if (From->Get(&Ptr, &Len)) { Set(Ptr, Len); SetName(From->GetName()); SetMimeType(From->GetMimeType()); SetContentId(From->GetContentId()); SetCharset(From->GetCharset()); } } } Attachment::Attachment(ScribeWnd *App, LDataI *object, const char *Import) : Thing(App) { _New(object); if (Import) ImportFile(Import); } Attachment::~Attachment() { DeleteObj(Msg); if (Owner) Owner->Attachments.Delete(this); } bool Attachment::ImportFile(const char *FileName) { LAutoPtr f(new LFile); if (!f) { LAssert(!"Out of memory"); return false; } LString Mime = ScribeGetFileMimeType(FileName); if (!f->Open(FileName, O_READ)) { LAssert(!"Can't open file."); return false; } char *c = strrchr((char*)FileName, DIR_CHAR); if (c) SetName(c + 1); else SetName(FileName); if (Mime) SetMimeType(Mime); LAutoStreamI s(f.Release()); GetObject()->SetStream(s); return true; } bool Attachment::ImportStream(const char *FileName, const char *MimeType, LAutoStreamI Stream) { if (!FileName || !MimeType || !Stream) { LAssert(!"Parameter error"); return false; } char *c = strrchr((char*)FileName, DIR_CHAR); if (c) SetName(c + 1); else SetName(FileName); SetMimeType(MimeType); GetObject()->SetStream(Stream); return true; } void Attachment::SetOwner(Mail *msg) { Owner = msg; } bool Attachment::CallMethod(const char *MethodName, LVariant *Ret, LArray &Args) { ScribeDomType Fld = StrToDom(MethodName); *Ret = false; switch (Fld) { case SdSave: // Type: (String FileName) { auto Fn = Args.Length() > 0 ? Args[0]->Str() : NULL; if (Fn) *Ret = SaveTo(Fn, true); return true; } default: break; } return Thing::CallMethod(MethodName, Ret, Args); } bool Attachment::GetVariant(const char *Name, LVariant &Value, const char *Array) { ScribeDomType Fld = StrToDom(Name); switch (Fld) { case SdLength: // Type: Int64 { Value = GetSize(); break; } case SdName: // Type: String { Value = GetName(); break; } case SdMimeType: // Type: String { Value = GetMimeType(); break; } case SdContentId: // Type: String { Value = GetContentId(); break; } case SdData: // Type: Binary { LAutoPtr s(GotoObject(_FL)); if (!s) return false; Value.Empty(); Value.Type = GV_BINARY; if ((Value.Value.Binary.Data = new char[Value.Value.Binary.Length = (int)s->GetSize()])) s->Read(Value.Value.Binary.Data, Value.Value.Binary.Length); break; } case SdType: // Type: Int32 { Value = GetObject()->Type(); break; } default: { return false; } } return true; } Thing &Attachment::operator =(Thing &t) { LAssert(0); return *this; } void IncFileIndex(char *FilePath, size_t FilePathLen) { char File[MAX_PATH_LEN]; char Ext[256] = ""; // Get the extension part char *Dot = strrchr(FilePath, '.'); if (Dot) { strcpy_s(Ext, sizeof(Ext), Dot); } // Get the filename part ssize_t FileLen = (ssize_t)strlen(FilePath) - strlen(Ext); memcpy(File, FilePath, FileLen); File[FileLen] = 0; // Seek to start of digits char *Digits = File + strlen(File) - 1; while (IsDigit(*Digits) && Digits > File) { Digits--; } if (!IsDigit(*Digits)) Digits++; // Increment the index int Index = atoi(Digits); sprintf_s(Digits, sizeof(File)-(Digits-File), "%i", Index + 1); // Write the resulting filename sprintf_s(FilePath, FilePathLen, "%s%s", File, Ext); } void Attachment::SetMsg(Mail *m) { Msg = m; } Mail *Attachment::GetMsg() { if (!Msg && IsMailMessage()) { // is an email LAutoStreamI f = GetObject()->GetStream(_FL); if (f) { if ((Msg = new Mail(App))) { Msg->SetWillDirty(false); Msg->App = App; Msg->ParentFile = this; Msg->OnAfterReceive(f); } } } return Msg; } bool Attachment::GetIsResizing() { return IsResizing; } void Attachment::SetIsResizing(bool b) { IsResizing = b; Update(); } bool Attachment::IsMailMessage() { return GetMimeType() && !_stricmp(GetMimeType(), sMimeMessage); } bool Attachment::IsVCalendar() { return GetMimeType() && ( !_stricmp(GetMimeType(), sMimeVCalendar) || !_stricmp(GetMimeType(), sMimeICalendar) ); } bool Attachment::IsVCard() { return GetMimeType() && !_stricmp(GetMimeType(), sMimeVCard); } char *Attachment::GetDropFileName() { if (!DropFileName) DropFileName = MakeFileName(); return DropFileName; } bool Attachment::GetDropFiles(LString::Array &Files) { bool Status = false; if (GetDropFileName()) { char p[MAX_PATH_LEN]; LMakePath(p, sizeof(p), ScribeTempPath(), DropFileName); if (SaveTo(p, true)) { Files.Add(p); Status = true; } } return Status; } LAutoString Attachment::MakeFileName() { auto Name = GetName(); LAutoString CleanName; if (Name) { CleanName.Reset(StripPath(Name)); CleanFileName(CleanName); } else { LArray Ext; char s[256] = "Attachment"; auto MimeType = GetMimeType(); LGetMimeTypeExtensions(MimeType, Ext); if (Ext.Length()) { size_t len = strlen(s); sprintf_s(s+len, sizeof(s)-len, ".%s", Ext[0].Get()); } CleanName.Reset(NewStr(s)); } return CleanName; } void Attachment::OnOpen(LView *Parent, char *Dest) { bool VCal; if (GetMsg()) { // Open the mail message... Msg->DoUI(Owner); } else if ((VCal = IsVCalendar()) || IsVCard()) { // Open the event or contact... Thing *c = App->CreateItem(VCal ? MAGIC_CALENDAR : MAGIC_CONTACT, 0, false); if (c) { LAutoPtr f(GotoObject(_FL)); if (f) { if (c->Import(f, GetMimeType())) { c->DoUI(); } else LgiMsg(Parent, "Failed to parse calendar.", "Error"); } else LgiTrace("%s:%i - Failed to get attachment stream.\n", _FL); } } else // is some generic file { bool IsExe = false; int TnefSizeLimit = 8 << 20; LStream *TnefStream = 0; LArray TnefIndex; int64 AttachPos = -1; LAutoStreamI f = GetObject()->GetStream(_FL); if (f) { IsExe = LIsFileExecutable(GetName(), f, AttachPos = f->GetPos(), f->GetSize()); if (!IsExe && f->GetSize() < TnefSizeLimit) { f->SetPos(AttachPos); if (!TnefReadIndex(f, TnefIndex)) { DeleteObj(TnefStream); } } f.Reset(); } if (IsExe) { LgiMsg(Parent, LLoadString(IDS_ERROR_EXE_FILE), AppName); return; } // Check for TNEF if (TnefStream) { LStringPipe p; for (unsigned n=0; nSize); p.Print("\t%s (%s)\n", TnefIndex[n]->Name, Size); } char *FileList = p.NewStr(); if (Owner && LgiMsg(Parent, LLoadString(IDS_ASK_TNEF_DECODE), AppName, MB_YESNO, FileList) == IDYES) { char *Tmp = ScribeTempPath(); if (Tmp) { for (unsigned i=0; iName); LFile Out; if (Out.Open(s, O_WRITE)) { Out.SetSize(0); if (TnefExtract(TnefStream, &Out, TnefIndex[i])) { Out.Close(); Attachment *NewFile = 0; Owner->AttachFile(NewFile = new Attachment(App, GetObject()->GetStore()->Create(MAGIC_ATTACHMENT), s)); if (NewFile && Owner->GetUI()) { AttachmentList *Lst = Owner->GetUI()->GetAttachments(); if (Lst) { Lst->Insert(NewFile); Lst->ResizeColumnsToContent(); } } } Out.Close(); } } } Owner->Save(); OnDeleteAttachment(Parent, false); } DeleteObj(TnefStream); DeleteArray(FileList); } else { // Open file... LAutoString FileToExecute; char *Tmp = ScribeTempPath(); if (!Tmp) return; // get the file name char FileName[MAX_PATH_LEN]; LAutoString CleanName = MakeFileName(); if (CleanName) { LMakePath(FileName, sizeof(FileName), Tmp, CleanName); while (LFileExists(FileName)) { IncFileIndex(FileName, sizeof(FileName)); } // write the file out if (SaveTo(FileName)) { FileToExecute.Reset(NewStr(FileName)); } } // open the file auto Mime = GetMimeType(); if (FileToExecute) { LAutoString AssociatedApp; LXmlTag *FileTypes = App->GetOptions()->LockTag(OPT_FileTypes, _FL); if (FileTypes) { auto Mime = GetMimeType(); auto FileName = GetName(); for (auto t: FileTypes->Children) { char *mt = t->GetAttr("mime"); char *ext = t->GetAttr("extension"); bool MimeMatch = mt && Mime && !_stricmp(mt, Mime); bool ExtMatch = ext && FileName && MatchStr(ext, FileName); if (MimeMatch || ExtMatch) { AssociatedApp.Reset(TrimStr(t->GetContent())); break; } } App->GetOptions()->Unlock(); } if (AssociatedApp) { const char *s = AssociatedApp; LAutoString Exe(LTokStr(s)); if (Exe) { char Args[MAX_PATH_LEN+100]; if (!strchr(s, '%') || sprintf_s(Args, sizeof(Args), s, FileToExecute.Get()) < 0) { if (sprintf_s(Args, sizeof(Args), "\"%s\"", FileToExecute.Get()) < 0) Args[0] = 0; } if (Args[0] && LExecute(Exe, Args)) { // Successful.. return; } } } if (!LExecute(FileToExecute, 0, Tmp)) { // if the default open fails.. open as text LString AppPath = LGetAppForMimeType(Mime ? Mime : sTextPlain); bool Status = false; if (AppPath) { char *s = strchr(AppPath, '%'); if (s) s[0] = 0; Status = LExecute(AppPath, FileToExecute, Tmp); } if (!Status) { LgiMsg(Parent, "Couldn't open file.", AppName, MB_OK); } } } } } } void Attachment::OnDeleteAttachment(LView *Parent, bool Ask) { if (Owner) { LVariant ConfirmDelete; App->GetOptions()->GetValue(OPT_ConfirmDelete, ConfirmDelete); if (!Ask || !ConfirmDelete.CastInt32() || LgiMsg(GetList(), LLoadString(IDS_DELETE_ASK), AppName, MB_YESNO) == IDYES) { List Sel; LList *p = GetList(); if (p) { p->GetSelection(Sel); } else { Sel.Insert(this); } for (LListItem *i: Sel) { Attachment *a = dynamic_cast(i); if (a) { if (p) { p->Remove(i); } a->Owner->DeleteAttachment(a); } } if (p) { p->Invalidate(); } } } } bool Attachment::SaveTo(char *FileName, bool Quite, LView *Parent) { bool Status = false; if (FileName) { LFile Out; Status = true; if (LFileExists(FileName)) { if (Quite) { return true; } else { Out.Close(); LString Msg = AskOverwriteMsg(FileName); Status = LgiMsg(Parent ? Parent : App, Msg, AppName, MB_YESNO) == IDYES; } } if (!Out.Open(FileName, O_WRITE)) { if (!Quite) LgiMsg(App, LLoadString(IDS_ERROR_CANT_WRITE), AppName, MB_OK, FileName); else LgiTrace("%s:%i - Can't open '%s' for writing (err=0x%x)\n", _FL, FileName, Out.GetError()); Status = false; } if (Status) { Out.SetSize(0); if (GetObject()) { int BufSize = 64 << 10; char *Buf = new char[BufSize]; LStreamI *f = GotoObject(_FL); if (f && Buf) { f->SetPos(0); Out.SetSize(0); int64 MySize = f->GetSize(); int64 s = MySize; while (s > 0) { ssize_t r = (int)MIN(BufSize, s); r = f->Read(Buf, r); if (r > 0) { Out.Write(Buf, r); s -= r; } else break; } int64 OutPos = Out.GetPos(); if (OutPos < MySize) { // Error writing to disk... Out.Close(); FileDev->Delete(FileName, false); LAssert(!"Failed to write whole attachment to disk."); Status = false; } } else { LgiTrace("%s:%i - GotoObject failed.\n", _FL); Status = false; } DeleteObj(f); DeleteArray(Buf); } /* else if (Data) { int Written = Out.Write(Data, Size); Status = Written == Size; } */ } } return Status; } void Attachment::OnSaveAs(LView *Parent) { auto Name = GetName(); char *n = StripPath(Name); if (!n) { n = NewStr("untitled"); } if (n) { CleanFileName(n); auto Select = new LFileSelect(Parent); Select->Type("All files", LGI_ALL_FILES); Select->Name(n); List Files; if (LListItem::Parent) { LListItem::Parent->GetSelection(Files); } else { Files.Insert(this); } if (Files.Length() > 0) { auto DoSave = [this, Files, Parent](LFileSelect *Select) { char Dir[MAX_PATH_LEN]; strcpy_s(Dir, sizeof(Dir), Select->Name()); if (Files.Length() > 1) { // Loop through all the files and write them to that directory for (unsigned idx=0; idx(i); if (a) { char Path[MAX_PATH_LEN]; auto d = StripPath(a->GetName()); if (d) { sprintf_s(Path, sizeof(Path), "%s%s%s", Dir, DIR_STR, d); a->SaveTo(Path); DeleteArray(d); } } } } else { // Write the file Attachment *a = dynamic_cast(Files[0]); if (a) { a->SaveTo(Dir, false, Parent); } } }; if (Files.Length() > 1) { // multiple files, ask which directory to write to Select->OpenFolder([DoSave](auto dlg, auto status) { if (status) DoSave(dlg); delete dlg; }); } else { // single file, ask for filename and path Select->Save([DoSave](auto dlg, auto status) { if (status) DoSave(dlg); delete dlg; }); } } DeleteArray(n); } } bool Attachment::OnKey(LKey &k) { if (k.vkey == LK_RETURN && k.IsChar) { if (k.Down()) { OnOpen(GetList()); } return true; } return false; } void Attachment::OnMouseClick(LMouse &m) { auto mt = GetMimeType(); bool OpenAttachment = false; if (m.IsContextMenu()) { // open the right click menu LSubMenu RClick; LString MimeType = GetMimeType(); RClick.AppendItem(LLoadString(IDS_ADD_TO_CAL), IDM_ADD_TO_CAL, IsVCalendar()); RClick.AppendSeparator(); RClick.AppendItem(LLoadString(IDS_OPEN), IDM_OPEN, true); RClick.AppendItem(LLoadString(IDS_SAVEAS), IDM_SAVEAS, true); RClick.AppendItem(LLoadString(IDS_DELETE), IDM_DELETE, true); RClick.AppendSeparator(); RClick.AppendItem(LLoadString(IDS_RESIZE), IDM_RESIZE, MimeType.Lower().Find("image/") >= 0); if (Parent->GetMouse(m, true)) { switch (RClick.Float(Parent, m.x, m.y)) { case IDM_OPEN: { OpenAttachment = true; break; } case IDM_DELETE: { OnDeleteAttachment(Parent, true); break; } case IDM_SAVEAS: { OnSaveAs(Parent); break; } case IDM_ADD_TO_CAL: { ScribeFolder *Cal = App->GetFolder(FOLDER_CALENDAR); if (!Cal) LgiMsg(Parent, "Can't find the calendar folder.", AppName); else { Thing *c = App->CreateItem(MAGIC_CALENDAR, 0, false); if (c) { LAutoPtr f(GotoObject(_FL)); if (f) { if (c->Import(c->AutoCast(f), mt)) { c->Save(Cal); c->DoUI(); } else LgiTrace("%s:%i - Failed to import cal stream.\n", _FL); } else LgiTrace("%s:%i - Failed to get attachment stream.\n", _FL); } else LgiTrace("%s:%i - Failed to create calendar obj.\n", _FL); } break; } case IDM_RESIZE: { List Sel; if (GetList() && GetList()->GetSelection(Sel)) { for (auto a: Sel) { if (!a->Owner) { LgiTrace("%s:%i - No owner?", _FL); break; } a->Owner->ResizeImage(a); } GetList()->ResizeColumnsToContent(); } break; } } } } else if (m.Double()) { OpenAttachment = true; } if (OpenAttachment) { // open the attachment OnOpen(Parent); } } bool Attachment::GetFormats(LDragFormats &Formats) { Formats.SupportsFileDrops(); return Formats.Length() > 0; } bool Attachment::GetData(LArray &Data) { int SetCount = 0; for (unsigned idx=0; idx Att; if (Parent->GetSelection(Att)) { LString::Array Files; for (auto a: Att) { // char *Nm = a->GetName(); if (!a->DropSourceFile || !LFileExists(a->DropSourceFile)) { a->DropSourceFile.Reset(); char p[MAX_PATH_LEN]; LAutoString Clean = a->MakeFileName(); LMakePath(p, sizeof(p), ScribeTempPath(), Clean); char Ext[256]; char *d = strrchr(p, '.'); if (!d) d = p + strlen(p); strcpy_s(Ext, sizeof(Ext), d); for (int i=1; LFileExists(p); i++) { sprintf_s(d, sizeof(p)-(d-p), "_%i%s", i, Ext); } if (a->SaveTo(p, true)) { a->DropSourceFile.Reset(NewStr(p)); } } if (a->DropSourceFile) { Files.Add(a->DropSourceFile.Get()); } else LAssert(0); } if (Files.First()) { LMouse m; App->GetMouse(m, true); if (CreateFileDrop(&dd, m, Files)) { SetCount++; } } } } } return SetCount > 0; } LStreamI *Attachment::GotoObject(const char *file, int line) { if (!GetObject()) return 0; LAutoStreamI s = GetObject()->GetStream(file, line); return s.Release(); } char *GetSubField(char *s, char *Field, bool AllowConversion = true) { char *Status = 0; if (s && Field) { s = strchr(s, ';'); if (s) { s++; size_t FieldLen = strlen(Field); char White[] = " \t\r\n"; while (*s) { // Skip leading whitespace while (*s && (strchr(White, *s) || *s == ';')) s++; // Parse field name if (IsAlpha(*s)) { char *f = s; while (*s && (IsAlpha(*s) || *s == '-')) s++; bool HasField = ((s-f) == FieldLen) && (_strnicmp(Field, f, FieldLen) == 0); while (*s && strchr(White, *s)) s++; if (*s == '=') { s++; while (*s && strchr(White, *s)) s++; if (*s && strchr("\'\"", *s)) { // Quote Delimited Field char d = *s++; char *e = strchr(s, d); if (e) { if (HasField) { if (AllowConversion) { Status = DecodeRfc2047(NewStr(s, e-s)); } else { Status = NewStr(s, e-s); } break; } s = e + 1; } else break; } else { // Delimited Field char *e = s; while (*e && *e != ';') e++; if (HasField) { Status = DecodeRfc2047(NewStr(s, e-s)); break; } s = e; } } else break; } else break; } } } return Status; } const char *Attachment::GetText(int i) { if (FieldArray.Length()) { return "This is an attachment!!!!!"; } else { switch (i) { case 0: { auto Nm = GetName(); if (IsResizing) { Buf.Printf("%s (%s)", Nm, LLoadString(IDS_RESIZING)); return Buf; } return Nm; } case 1: { static char s[64]; LFormatSize(s, sizeof(s), GetSize()); return s; } case 2: { return GetMimeType(); } case 3: { return GetContentId(); } } } return 0; } bool Attachment::Get(char **ptr, ssize_t *size) { if (!ptr || !size) return false; LStreamI *f = GotoObject(_FL); if (!f) return false; *size = f->GetSize(); *ptr = new char[*size+1]; if (*ptr) { auto r = f->Read(*ptr, *size); (*ptr)[r] = 0; } DeleteObj(f); return true; } bool Attachment::Set(LAutoStreamI Stream) { if (!GetObject()) { LAssert(0); return false; } if (!GetObject()->SetStream(Stream)) { LAssert(0); return false; } return true; } bool Attachment::Set(char *ptr, ssize_t size) { LAutoStreamI s(new LMemStream(ptr, size)); return Set(s); } diff --git a/src/ScribeFilter.cpp b/src/ScribeFilter.cpp --- a/src/ScribeFilter.cpp +++ b/src/ScribeFilter.cpp @@ -1,4214 +1,4213 @@ /* ** FILE: ScribeFilter.cpp ** AUTHOR: Matthew Allen ** DATE: 29/10/1999 ** DESCRIPTION: Scribe filters ** ** Copyright (C) 1999-2022, Matthew Allen ** fret@memecode.com ** */ #include #include #include #include #include #include "Scribe.h" -#include "lgi/common/NetTools.h" #include "lgi/common/Combo.h" #include "lgi/common/Edit.h" #include "lgi/common/Button.h" #include "lgi/common/ProgressDlg.h" #include "lgi/common/FilterUi.h" #include "lgi/common/XmlTree.h" #include "lgi/common/ClipBoard.h" #include "lgi/common/TabView.h" #include "lgi/common/Printer.h" #include "lgi/common/LgiRes.h" #include "lgi/common/FileSelect.h" #include "lgi/common/Charset.h" #include "resdefs.h" #include "resource.h" #define COMBINE_OP_AND 0 #define COMBINE_OP_OR 1 #define IDM_TRUE 500 #define IDM_FALSE 501 #define IDM_NOTNEW 502 #define IDM_LOCAL 503 #define IDM_SERVER 504 #define IDM_LOCAL_AND_SERVER 505 const char *ATTR_NOT = "Not"; const char *ATTR_FIELD = "Field"; const char *ATTR_OP = "Op"; const char *ATTR_VALUE = "Value"; const char *ELEMENT_AND = "And"; const char *ELEMENT_OR = "Or"; const char *ELEMENT_CONDITION = "Condition"; const char *ELEMENT_CONDITIONS = "Conditions"; const char *ELEMENT_ACTION = "Action"; #define SkipWs(s) while ((*s) && strchr(WhiteSpace, *s)) s++; ////////////////////////////////////////////////////////////// class FilterPrivate { public: bool *Stop; LStream *Log; FilterPrivate() { Stop = 0; Log = 0; } }; ////////////////////////////////////////////////////////////// // Filter field definitions ItemFieldDef FilterFieldDefs[] = { {"Name", SdName, GV_STRING, FIELD_FILTER_NAME}, {"Index", SdIndex, GV_INT32, FIELD_FILTER_INDEX}, {"Incoming", SdIncoming, GV_INT32, FIELD_FILTER_INCOMING}, {"Outgoing", SdOutgoing, GV_INT32, FIELD_FILTER_OUTGOING}, {"Internal", SdInternal, GV_INT32, FIELD_FILTER_INTERNAL}, {0} }; int DefaultFilterFields[] = { FIELD_FILTER_NAME, FIELD_FILTER_INDEX, FIELD_FILTER_INCOMING, FIELD_FILTER_OUTGOING, FIELD_FILTER_INTERNAL, 0 }; #define ForCondField(Macro, Value) \ switch (Value) \ { \ case 0: Macro("To"); break; \ case 1: Macro("From"); break; \ case 2: Macro("Subject"); break; \ case 3: Macro("Size"); break; \ case 4: Macro("DateReceived"); break; \ case 5: Macro("DateSent"); break; \ case 6: Macro("Body"); break; \ case 7: Macro("InternetHeaders"); break; \ case 8: Macro("MessageID"); break; \ case 9: Macro("Priority"); break; \ case 10: /* flags */ break; \ case 11: Macro("Html"); break; \ case 12: Macro("Label"); break; \ case 13: Macro("From.Contact"); break; \ case 14: Macro("ImapCacheFile"); break; \ case 15: Macro("ImapFlags"); break; \ case 16: Macro("Attachments"); break; \ case 17: Macro("AttachmentNames"); break; \ case 18: Macro("From.Groups"); break; \ case 19: Macro("*"); break; \ } // Macro("", FIELD_FLAGS) struct ActionName { int Id; const char *Default; }; ActionName ActionNames[] = { {IDS_ACTION_MOVE_FOLDER, "Move to Folder"}, {IDC_DELETE, "Delete"}, {IDS_PRINT, "Print"}, {IDS_ACTION_SOUND, "Play Sound"}, {IDS_ACTION_OPEN, "Open Email"}, {IDS_ACTION_EXECUTE, "Execute Process"}, {IDS_ACTION_SET_COLOUR, "Set Colour"}, {IDS_SET_READ, "Set Read"}, {IDS_ACTION_SET_LABEL, "Set Label"}, {IDS_ACTION_EMPTY_FOLDER, "Empty Folder"}, {IDS_ACTION_MARK_SPAM, "Mark As Spam"}, {IDS_REPLY, "Reply"}, {IDS_FORWARD, "Forward"}, {IDS_BOUNCE, "Bounce"}, {IDS_ACTION_SAVE_ATTACHMENTS, "Save Attachment(s)"}, {IDS_ACTION_DELETE_ATTACHMENTS, "Delete Attachments(s)"}, {L_CHANGE_CHARSET, "Change Charset"}, {IDS_ACTION_COPY, "Copy to Folder"}, {IDS_EXPORT, "Export"}, {0, 0}, {IDS_ACTION_CREATE_FOLDER, "Create Folder"}, {0, 0} }; // These are the english names used for storing XML const char *OpNames[] = { "=", "!=", "<", "<=", ">=", ">", "Like", // LLoadString(IDS_LIKE), "Contains", // LLoadString(IDS_CONTAINS), "Starts With", // LLoadString(IDS_STARTS_WITH), "Ends With", // LLoadString(IDS_ENDS_WITH), 0 }; const char *TranslatedOpNames[] = { "=", "!=", "<", "<=", ">=", ">", 0, // like 0, // contains 0, // starts with 0, // ends with 0 }; const char **GetOpNames(bool Translated) { if (Translated) { if (TranslatedOpNames[6] == NULL) { TranslatedOpNames[6] = LLoadString(IDS_LIKE); TranslatedOpNames[7] = LLoadString(IDS_CONTAINS); TranslatedOpNames[8] = LLoadString(IDS_STARTS_WITH); TranslatedOpNames[9] = LLoadString(IDS_ENDS_WITH); } if (TranslatedOpNames[6]) { return TranslatedOpNames; } } return OpNames; } ////////////////////////////////////////////////////////////// void SkipSep(const char *&s) { while (s && *s && strchr(" \t,", *s)) s++; } LCombo *LoadTemplates(ScribeWnd *App, LView *Wnd, List &MsgIds, int Ctrl, char *Template) { LCombo *Temp; if (Wnd->GetViewById(IDC_TEMPLATE, Temp)) { ScribeFolder *Templates = App->GetFolder(FOLDER_TEMPLATES); if (Templates) { int n=0; for (auto t: Templates->Items) { Mail *m = t->IsMail(); if (m) { auto Id = m->GetMessageId(true); if (Id) { MsgIds.Insert(NewStr(Id)); Temp->Insert(m->GetSubject() ? m->GetSubject() : (char*)"(no subject)"); if (Template && strcmp(Template, Id) == 0) { Temp->Value(n); } } } n++; } } } return Temp; } class BrowseReply : public LDialog { List MsgIds; LCombo *Temp; public: LString Arg; BrowseReply(ScribeWnd *App, LView *Parent, const char *arg) { SetParent(Parent); LoadFromResource(IDD_FILTER_REPLY); MoveToCenter(); char *Template = LTokStr(arg); SkipSep(arg); char *All = LTokStr(arg); SkipSep(arg); char *MarkReplied = LTokStr(arg); if (All) { SetCtrlValue(IDC_ALL, atoi(All)); } if (MarkReplied) { SetCtrlValue(IDC_MARK_REPLIED, atoi(MarkReplied)); } Temp = LoadTemplates(App, this, MsgIds, IDC_TEMPLATE, Template); DeleteArray(Template); DeleteArray(MarkReplied); DeleteArray(All); } ~BrowseReply() { MsgIds.DeleteArrays(); } int OnNotify(LViewI *c, LNotification n) { switch (c->GetId()) { case IDOK: { if (Temp) { char *Template = MsgIds[(int)Temp->Value()]; int All = (int)GetCtrlValue(IDC_ALL); int MarkReplied = (int)GetCtrlValue(IDC_MARK_REPLIED); char s[256]; sprintf_s(s, sizeof(s), "\"%s\" %i %i", Template?Template:(char*)"", All, MarkReplied); Arg = s; } // Fall thru } case IDCANCEL: { EndModal(c->GetId() == IDOK); break; } } return 0; } }; class BrowseForward : public LDialog { List MsgIds; ScribeWnd *App; LCombo *Temp; bool UseTemplate; public: LString Arg; BrowseForward(ScribeWnd *app, LView *parent, const char *arg, bool temp) { UseTemplate = temp; App = app; Arg = 0; Temp = 0; SetParent(parent); LoadFromResource(IDD_FILTER_FORWARD); MoveToCenter(); char *Template = 0; if (UseTemplate) { Template = LTokStr(arg); SkipSep(arg); } char *Email = LTokStr(arg); SkipSep(arg); char *Forward = LTokStr(arg); SkipSep(arg); char *MarkForwarded = LTokStr(arg); if (UseTemplate) { bool HasTemplate = ValidStr(Template) ? strlen(Template) > 1 : 0; SetCtrlValue(IDC_USE_TEMPLATE, HasTemplate); Temp = LoadTemplates(App, this, MsgIds, IDC_TEMPLATE, HasTemplate ? Template : 0); } else { LViewI *v = FindControl(IDC_USE_TEMPLATE); if (v) { int y1 = v->GetPos().y1; Children.Delete(v); DeleteObj(v); v = FindControl(IDC_TEMPLATE); if (v) { int y2 = v->GetPos().y2; Children.Delete(v); DeleteObj(v); int Sub = y1 - y2 - 10; for (auto v: Children) { LRect r = v->GetPos(); r.Offset(0, Sub); v->SetPos(r); } LRect r = GetPos(); r.y2 += Sub; SetPos(r); } } } SetCtrlName(IDC_EMAIL, Email); if (Forward) SetCtrlValue(IDC_ATTACHMENTS, atoi(Forward)); if (MarkForwarded) SetCtrlValue(IDC_MARK_FORWARDED, atoi(MarkForwarded)); DeleteArray(Template); DeleteArray(Email); DeleteArray(Forward); DeleteArray(MarkForwarded); } ~BrowseForward() { DeleteArray(Arg); } int OnNotify(LViewI *c, LNotification n) { switch (c->GetId()) { case IDOK: { char s[256]; bool HasTemplate = GetCtrlValue(IDC_USE_TEMPLATE) != 0; char *MsgId = HasTemplate ? MsgIds[(int)GetCtrlValue(IDC_TEMPLATE)] : 0; const char *Email = GetCtrlName(IDC_EMAIL); int Attachments = (int)GetCtrlValue(IDC_ATTACHMENTS); int MarkForwarded = (int)GetCtrlValue(IDC_MARK_FORWARDED); if (UseTemplate) { sprintf_s(s, sizeof(s), "\"%s\" \"%s\" %i %i", MsgId, Email?Email:(char*)"", Attachments, MarkForwarded); } else { sprintf_s(s, sizeof(s), "\"%s\" %i %i", Email?Email:(char*)"", Attachments, MarkForwarded); } Arg = s; // Fall thru } case IDCANCEL: { EndModal(c->GetId() == IDOK); } } return 0; } }; class BrowseSaveAttach : public LDialog { ScribeWnd *App; public: char *Arg; BrowseSaveAttach(ScribeWnd *app, LView *parent, const char *arg) { Arg = 0; App = app; SetParent(parent); if (LoadFromResource(IDD_FILTER_SAVE_ATTACH)) { MoveToCenter(); char *Dir = LTokStr(arg); SkipSep(arg); char *Types = LTokStr(arg); SetCtrlName(IDC_DIR, Dir); SetCtrlName(IDC_TYPES, Types); DeleteArray(Dir); DeleteArray(Types); } } ~BrowseSaveAttach() { DeleteArray(Arg); } int OnNotify(LViewI *c, LNotification n) { switch (c->GetId()) { case IDC_BROWSE_DIR: { auto s = new LFileSelect(this); s->Name(GetCtrlName(IDC_DIR)); s->OpenFolder([this](auto s, auto status) { if (status) SetCtrlName(IDC_DIR, s->Name()); delete s; }); break; } case IDOK: { char s[512]; const char *Dir = GetCtrlName(IDC_DIR); const char *Types = GetCtrlName(IDC_TYPES); sprintf_s(s, sizeof(s), "\"%s\",\"%s\"", Dir?Dir:(char*)"", Types?Types:(char*)""); Arg = NewStr(s); // Fall thru } case IDCANCEL: { EndModal(c->GetId() == IDOK); } } return 0; } }; bool LgiCreateTempFileName(char *Path, int PathLen) { if (Path) { #if defined WIN32 int Len = GetTempPathA(PathLen, Path); #else strcpy(Path, "/tmp"); int Len = (int)strlen(Path); #endif if (Path[Len-1] != DIR_CHAR) strcat(Path, DIR_STR); int len = (int) strlen(Path); sprintf_s(Path+len, PathLen-len, "~%i.txt", LRand(10000)); return true; } return false; } ////////////////////////////////////////////////////////////// FilterCondition::FilterCondition() { Op = 0; Not = false; } FilterCondition &FilterCondition::operator=(FilterCondition &c) { Source.Reset(NewStr(c.Source)); Op = c.Op; Not = c.Not; Value.Reset(NewStr(c.Value)); return *this; } bool FilterCondition::Test(Filter *F, Mail *m, LStream *Log) { if (Log) Log->Print("\tCondition.Test Fld='%s'\n", (char*)Source); if (ValidStr(Source)) { int Flds = 0; for (; MailFieldDefs[Flds].FieldId; Flds++) { } LVariant v; // Get data if (_stricmp(Source, "mail.attachments") == 0) { // attachment(s) data List Attachments; if (m->GetAttachments(&Attachments)) { for (auto a: Attachments) { char *Data; ssize_t Length; LDateTime Temp; if (a->Get(&Data, &Length)) { // Zero terminate the string LVariant v; v.SetBinary(Length, Data); // Test the file if (TestData(F, v, Log)) { return true; } } } } return false; } else if (_stricmp(Source, "mail.attachmentnames") == 0) { // attachment(s) name List Attachments; // ItemFieldDef AttachType = {"Attachment(s) Name", SdAttachmentNames, GV_STRING}; LDateTime Temp; if (m->GetAttachments(&Attachments)) { for (auto a: Attachments) { LVariant v = a->GetName(); if (TestData(F, v, Log)) { return true; } } } return false; } else { bool Status = false; ItemFieldDef *f = 0; if (_stricmp(Source, "mail.*") == 0) { ItemFieldDef *Start = MailFieldDefs; ItemFieldDef *End = MailFieldDefs + Flds - 1; for (f = Start; f <= End && !Status; f++) { switch (f->FieldId) { case FIELD_TO: { for (LDataPropI *a = m->GetTo()->First(); a; a = m->GetTo()->Next()) { char Data[256]; sprintf_s(Data, sizeof(Data), "%s <%s>", a->GetStr(FIELD_NAME), a->GetStr(FIELD_EMAIL)); LVariant v(Data); if (TestData(F, v, Log)) { Status |= true; } } continue; break; } case FIELD_FROM: { char Data[256]; sprintf_s(Data, sizeof(Data), "%s <%s>", m->GetFrom()->GetStr(FIELD_NAME), m->GetFrom()->GetStr(FIELD_EMAIL)); v = Data; break; } case FIELD_REPLY: { char Data[256]; sprintf_s(Data, sizeof(Data), "%s <%s>", m->GetReply()->GetStr(FIELD_NAME), m->GetReply()->GetStr(FIELD_EMAIL)); v = Data; break; } case FIELD_SUBJECT: v = m->GetSubject(); break; case FIELD_SIZE: v = m->TotalSizeof(); break; case FIELD_DATE_RECEIVED: v = m->GetDateReceived(); break; case FIELD_DATE_SENT: v = m->GetDateSent(); break; case FIELD_TEXT: v = m->GetBody(); break; case FIELD_INTERNET_HEADER: v = m->GetInternetHeader(); break; case FIELD_MESSAGE_ID: v = m->GetMessageId(); break; case FIELD_PRIORITY: v = m->GetPriority(); break; case FIELD_ALTERNATE_HTML: v = m->GetHtml(); break; case FIELD_LABEL: v = m->GetLabel(); break; } } } else { if (F) { F->GetValue(Source, v); } else if (Log) { Log->Print("%s:%i - Error: No filter to query value.\n", __FILE__, __LINE__); } } if (v.Type) { // Test data Status |= TestData(F, v, Log); } else if (Log) { Log->Print("%s:%i - Error: Variant doesn't have type!\n", __FILE__, __LINE__); } return Status; } } return false; } char *LogPreview(char *s) { LStringPipe p(1 << 10); if (s) { char *c; for (c = s; *c && c - s < 200; c++) { switch (*c) { case '\n': p.Push("\\n"); break; case '\r': p.Push("\\r"); break; case '\t': p.Push("\\t"); break; default: { p.Push(c, 1); } } } if (*c) { p.Push("..."); } } return p.NewStr(); } bool FilterCondition::TestData(Filter *F, LVariant &Var, LStream *Log) { // Do DOM lookup on the Value LVariant Val; if (F && F->Evaluate(Value, Val)) { // Compare using type switch (Var.Type) { case GV_LIST: { for (auto v: *Var.Value.Lst) { if (TestData(F, *v, Log)) { return true; } } break; } case GV_DOM: { // Probably an address field LVariant n; if (Var.Value.Dom->GetValue("Name", n)) { if (TestData(F, n, Log)) { return true; } } if (Var.Value.Dom->GetValue("Email", n)) { if (TestData(F, n, Log)) { return true; } } break; } case GV_STRING: { char *sVar = Var.Str(); char *sVal = Val.Str(); bool IsStr = ValidStr(sVar); bool IsVal = ValidStr(sVal); char *VarLog = Log ? LogPreview(sVar) : 0; bool m = false; switch (Op) { case OP_EQUAL: { if (!IsStr && !IsVal) { m = true; } else if (IsStr && IsVal) { m = _stricmp(sVal, sVar) == 0; } if (Log) Log->Print("\t\t\t'%s' == '%s' = %i\n", VarLog, sVal, m); break; } case OP_LIKE: { m = MatchStr(sVal, sVar); if (Log) Log->Print("\t\t\t'%s' like '%s' = %i\n", VarLog, sVal, m); break; } case OP_CONTAINS: { if (IsVal && IsStr) { m = stristr(sVar, sVal) != 0; if (Log) Log->Print("\t\t\t'%s' contains '%s' = %i\n", VarLog, sVal, m); } break; } case OP_STARTS_WITH: { if (IsVal && IsStr) { size_t Len = strlen(sVal); m = _strnicmp(sVar, sVal, Len) == 0; if (Log) Log->Print("\t\t\t'%s' starts with '%s' = %i\n", VarLog, sVal, m); } break; } case OP_ENDS_WITH: { if (IsVal && IsStr) { size_t SLen = strlen(sVar); size_t VLen = strlen(sVal); if (SLen >= VLen) { m = _strnicmp(sVar + SLen - VLen, sVal, VLen) == 0; if (Log) Log->Print("\t\t\t'%s' ends with '%s' = %i\n", VarLog, sVal, m); } else { if (Log) Log->Print("\t\t\tEnds With Error: '%s' is shorter than '%s'\n", sVar, sVal); } } else { if (Log) Log->Print("\t\t\tEnds With Error: invalid arguments\n"); } break; } } DeleteArray(VarLog); return m; break; } case GV_INT32: { // Convert Val to int int Int = 0; if (Val.Str()) { Int = atoi(Val.Str()); } else if (Val.Type == GV_INT32) { Int = Val.Value.Int; } int IntVal = Var.Value.Int; switch (Op) { case OP_LIKE: // for lack anything better case OP_EQUAL: { bool m = Int == IntVal; if (Log) Log->Print("\t\t\t%i == %i = %i\n", Int, IntVal, m); return m; } case OP_NOT_EQUAL: { bool m = Int != IntVal; if (Log) Log->Print("\t\t\t%i != %i = %i\n", Int, IntVal, m); return m; } case OP_LESS_THAN: { bool m = Int < IntVal; if (Log) Log->Print("\t\t\t%i < %i = %i\n", Int, IntVal, m); return m; } case OP_LESS_THAN_OR_EQUAL: { bool m = Int <= IntVal; if (Log) Log->Print("\t\t\t%i <= %i = %i\n", Int, IntVal, m); return m; } case OP_GREATER_THAN: { bool m = Int > IntVal; if (Log) Log->Print("\t\t\t%i > %i = %i\n", Int, IntVal, m); return m; } case OP_GREATER_THAN_OR_EQUAL: { bool m = Int >= IntVal; if (Log) Log->Print("\t\t\t%i >= %i = %i\n", Int, IntVal, m); return m; } } break; } case GV_DATETIME: { LDateTime Temp; LDateTime *DVal; if (Val.Type == GV_DATETIME) { DVal = Val.Value.Date; } else if (Val.Type == GV_STRING) { Temp.Set(Val.Str()); DVal = &Temp; } else break; LDateTime *DVar = Var.Value.Date; if (DVal && DVar) { bool Less = *DVar < *DVal; bool Greater = *DVar > *DVal; bool Equal = !Less && !Greater; char LogVal[64]; char LogVar[64]; if (Log) { DVal->Get(LogVal, sizeof(LogVal)); DVar->Get(LogVar, sizeof(LogVar)); } switch (Op) { case OP_LIKE: case OP_EQUAL: { if (Log) Log->Print("\t\t\t%s = %s == %i\n", LogVar, LogVal, Equal); return Equal; } case OP_NOT_EQUAL: { if (Log) Log->Print("\t\t\t%s != %s == %i\n", LogVar, LogVal, !Equal); return !Equal; } case OP_LESS_THAN: { if (Log) Log->Print("\t\t\t%s <= %s == %i\n", LogVar, LogVal, Less); return Less; } case OP_LESS_THAN_OR_EQUAL: { if (Log) Log->Print("\t\t\t%s < %s == %i\n", LogVar, LogVal, Less || Equal); return Less || Equal; } case OP_GREATER_THAN: { if (Log) Log->Print("\t\t\t%s > %s == %i\n", LogVar, LogVal, Greater); return Greater; } case OP_GREATER_THAN_OR_EQUAL: { if (Log) Log->Print("\t\t\t%s >= %s == %i\n", LogVar, LogVal, Greater || Equal); return Greater || Equal; } } } break; } default: { if (Log) Log->Print("\t\t\tUnknown data type %i.\n", Var.Type); break; } } } return false; } ThingUi *FilterCondition::DoUI(MailContainer *c) { return NULL; } ////////////////////////////////////////////////////////////// // #define OPT_Action "Action" #define OPT_Type "Type" #define OPT_Arg1 "Arg1" #define IDC_TYPE_CBO 2000 #define IDC_ARG_EDIT 2001 #define IDC_BROWSE_ARG 2002 FilterAction::FilterAction(LDataStoreI *Store) { Type = ACTION_MOVE_TO_FOLDER; TypeCbo = 0; ArgEdit = 0; Btn = 0; } FilterAction::~FilterAction() { DeleteObj(TypeCbo); DeleteObj(ArgEdit); DeleteObj(Btn); } int FilterAction::OnNotify(LViewI *c, LNotification n) { switch (c->GetId()) { case IDC_TYPE_CBO: { Type = (FilterActionTypes) c->Value(); break; } case IDC_ARG_EDIT: { Arg1 = c->Name(); break; } case IDC_BROWSE_ARG: { if (ArgEdit) ArgEdit->Name(Arg1); break; } } return 0; } void FilterAction::OnMeasure(LPoint *Info) { LListItem::OnMeasure(Info); if (Select()) Info->y += 2; } bool FilterAction::Select() { return LListItem::Select(); } void FilterAction::OnPaintColumn(LItem::ItemPaintCtx &Ctx, int i, LItemColumn *c) { LListItem::OnPaintColumn(Ctx, i, c); if (LListItem::Select() && !TypeCbo && !ArgEdit) Select(true); else if (i == 0 && TypeCbo) TypeCbo->SetPos(*GetPos(i)); else if (i == 1 && ArgEdit) ArgEdit->SetPos(*GetPos(i)); else if (i == 2 && Btn) Btn->SetPos(*GetPos(i)); } void FilterAction::Select(bool b) { LListItem::Select(b); if (b) { LList *Lst = LListItem::GetList(); if (Lst && Lst->IsAttached()) { LRect *r = GetPos(0); if (!TypeCbo) { TypeCbo = new LCombo(IDC_TYPE_CBO, r->x1, r->y1, r->X(), r->Y(), 0); for (int i=0; ActionNames[i].Id; i++) TypeCbo->Insert(LLoadString(ActionNames[i].Id)); TypeCbo->Attach(Lst); } TypeCbo->Value(Type); TypeCbo->SetPos(*r); r = GetPos(1); if (!ArgEdit) { ArgEdit = new LEdit(IDC_ARG_EDIT, r->x1, r->y1, r->X(), r->Y(), 0); ArgEdit->Attach(Lst); } ArgEdit->Name(Arg1); ArgEdit->SetPos(*r); r = GetPos(2); if (!Btn) { Btn = new LButton(IDC_BROWSE_ARG, r->x1, r->y1, r->X(), r->Y(), "..."); Btn->Attach(Lst); } Btn->SetPos(*r); } } else { DeleteObj(TypeCbo); DeleteObj(ArgEdit); DeleteObj(Btn); } } const char *FilterAction::GetText(int Col) { switch (Col) { case 0: return (char*)LLoadString(ActionNames[Type].Id); break; case 1: return Arg1; break; case 2: break; } return 0; } bool FilterAction::Get(LXmlTag *t) { if (!t) return false; // Obj -> XML t->SetAttr(OPT_Type, Type); t->SetAttr(OPT_Arg1, Arg1); return true; } bool FilterAction::Set(LXmlTag *t) { if (!t) return false; // XML -> Obj Type = (FilterActionTypes) t->GetAsInt(OPT_Type); Arg1 = t->GetAttr(OPT_Arg1); return true; } LDataPropI &FilterAction::operator =(LDataPropI &p) { FilterAction *c = dynamic_cast(&p); if (c) { Type = c->Type; Arg1 = c->Arg1; } return *this; } ThingUi *FilterAction::DoUI(MailContainer *c) { return 0; } const char *GetFileName(const char *Path) { if (Path) { auto d = strrchr(Path, DIR_CHAR); if (d) return d + 1; else return Path; } return 0; } bool CollectAttachmentsByPattern(Mail *m, LString Pattern, List &Files) { Files.Empty(); if (m) { List Attachments; if (m->GetAttachments(&Attachments)) { auto p = Pattern.SplitDelimit(" ,;"); for (auto a: Attachments) { bool Match = true; for (unsigned i=0; Match && iGetName()); if (d) Match = MatchStr(p[i], d); } if (Match) Files.Insert(a); } } } return Files[0] != 0; } Mail *GetTemplateMail(ScribeWnd *App, char *TemplateMsgId) { ScribeFolder *Templates = App->GetFolder(FOLDER_TEMPLATES); if (Templates) { // Mail *Template = 0; for (auto t: Templates->Items) { Mail *m = t->IsMail(); if (m) { auto MsgId = m->GetMessageId(); if (MsgId && strcmp(MsgId, TemplateMsgId) == 0) { return m; break; } } } } return 0; } class FilterScribeDom : public ScribeDom { public: FilterScribeDom(ScribeWnd *a) : ScribeDom(a) { } bool GetVariant(const char *Name, LVariant &Value, const char *Array = 0) { if (!Name) return false; if (!_stricmp(Name, "file")) { LVariant v; if (!GetValue(Array, v)) return false; char p[MAX_PATH_LEN], fn[32]; do { sprintf_s(fn, sizeof(fn), "file_%d.tmp", LRand()); LMakePath(p, sizeof(p), ScribeTempPath(), fn); } while (LFileExists(p)); LFile f; if (f.Open(p, O_WRITE)) { switch (v.Type) { case GV_INT32: f.Print("%i", v.Value.Int); break; case GV_INT64: f.Print(LPrintfInt64, v.Value.Int64); break; case GV_BOOL: f.Print("%s", v.Value.Bool ? "true" : "false"); break; case GV_DOUBLE: f.Print("%f", v.Value.Dbl); break; case GV_STRING: case GV_WSTRING: f.Print("%s", v.Str()); break; case GV_BINARY: f.Write(v.Value.Binary.Data, v.Value.Binary.Length); break; case GV_DATETIME: v.Value.Date->Get(fn, sizeof(fn)); f.Write(fn, strlen(fn)); break; default: f.Print("Unsupported type."); break; } f.Close(); Value = p; return true; } } return ScribeDom::GetVariant(Name, Value, Array); } }; bool FilterAction::Do(Filter *F, ScribeWnd *App, Mail *&m, LStream *Log) { bool Status = false; if (!F || !App || !m) { LAssert(!"Param error."); return false; } switch (Type) { case ACTION_MOVE_TO_FOLDER: { ScribeFolder *Folder = App->GetFolder(Arg1); if (Folder) { LArray Items; Items.Add(m); Folder->MoveTo(Items, false, [this, Log](auto result, auto status) { if (Log) Log->Print("\tACTION_MOVE_TO_FOLDER(%s) = %i.\n", Arg1.Get(), result); }); Status = true; } else if (Log) { Log->Print("\tACTION_MOVE_TO_FOLDER(%s) failed, folder missing.\n", Arg1.Get()); } break; } case ACTION_COPY: { ScribeFolder *Folder = App->GetFolder(Arg1); if (Folder) { LArray Items; Items.Add(m); Folder->MoveTo(Items, true, [this, Log](auto result, auto status) { if (Log) Log->Print("\tACTION_COPY(%s) = %i.\n", Arg1.Get(), result); }); Status = true; } else if (Log) { Log->Print("\tACTION_COPY(%s) failed, folder missing.\n", Arg1.Get()); } break; } case ACTION_EXPORT: { LFile::Path p(Arg1); if (!p.IsFolder()) { if (Log) Log->Print("\tACTION_EXPORT(%s) failed, folder missing.\n", Arg1.Get()); break; } auto Fn = m->GetDropFileName(); p += LGetLeaf(Fn); LAutoPtr out(new LFile); if (!out->Open(p, O_WRITE)) { if (Log) Log->Print("\tACTION_EXPORT(%s) failed: couldn't open file: %s.\n", Arg1.Get(), p.GetFull().Get()); break; } if (!m->Export(m->AutoCast(out), sMimeMessage)) { if (Log) Log->Print("\tACTION_EXPORT(%s) failed: couldn't export file.\n", Arg1.Get()); } break; } case ACTION_DELETE: { bool Local = ValidStr(Arg1) ? stristr(Arg1, "local") != 0 : true; bool Server = stristr(Arg1, "server") != 0; if (Server) { ScribeAccount *a = m->GetAccountSentTo(); if (!a) break; auto Uid = m->GetServerUid(); if (Uid.Str()) { if (Log) Log->Print("\tACTION_DELETE - Setting '%s' to be deleted on the server (Uid=%s)\n", m->GetSubject(), Uid.Str()); a->Receive.DeleteAsSpam(Uid.Str()); m->SetServerUid(Uid = NULL); } else { LVariant Uid; if (m->GetValue("InternetHeader[X-UIDL]", Uid)) { if (Log) Log->Print("\tACTION_DELETE - Setting '%s' to be deleted on the server (Uid=%s)\n", m->GetSubject(), Uid.Str()); a->Receive.DeleteAsSpam(Uid.Str()); } } } if (Local) { bool DeleteStatus = m->OnDelete(); if (Log) Log->Print("\tACTION_DELETE(%s) status %i.\n", Arg1.Get(), DeleteStatus); /* ScribeFolder *Folder = App->GetFolder(FOLDER_TRASH); if (Folder) { Thing *t = m; Status = Folder->MoveTo(t); m = t->IsMail(); if (Log) Log->Print("\tACTION_DELETE(%s) = %i.\n", Arg1.Get(), Status); } else if (Log) { Log->Print("\tACTION_DELETE(%s) failed, trash missing.\n", Arg1.Get()); } */ } break; } case ACTION_SET_READ: { bool Read = true; bool NotNew = false; if (ValidStr(Arg1)) { if (IsDigit(*Arg1)) { // boolean number Read = Arg1.Int() != 0; } else if (stristr(Arg1, "true")) { Read = true; } else if (stristr(Arg1, "false")) { Read = false; } if (stristr(Arg1, "notnew")) { NotNew = true; } } int f = m->GetFlags(); if (Read) SetFlag(f, MAIL_READ); else ClearFlag(f, MAIL_READ); m->SetFlags(f); if (Log) Log->Print("\tACTION_SET_READ(%s)\n", Arg1.Get()); if (NotNew) { List Objs; Objs.Insert(m); App->OnNewMail(&Objs, false); } break; } case ACTION_LABEL: { LVariant v; if (!F || !F->GetValue(Arg1, v)) v = Arg1; m->SetVariant("Label", v); break; } case ACTION_EMPTY_FOLDER: { if (F->App && Arg1) { ScribeFolder *Folder = F->App->GetFolder(Arg1); if (Folder) { Folder->LoadThings(); List m; Thing *t; while ((t = Folder->Items[0])) { if (t->IsMail()) { m.Insert(t->IsMail()); } if (Folder->DeleteThing(t)) { DeleteObj(t); } } F->App->OnNewMail(&m, false); Folder->OnUpdateUnRead(0, true); if (Log) Log->Print("\tACTION_EMPTY_FOLDER(%s)\n", Arg1.Get()); } } break; } case ACTION_MARK_AS_SPAM: { m->DeleteAsSpam(App); break; } case ACTION_PRINT: { LPrinter Info; #ifdef _MSC_VER #pragma message ("Warning: ACTION_PRINT not implemented.") #endif /* FIXME if (Info.Serialize(Arg1, false)) { App->ThingPrint(m, &Info); } */ break; } case ACTION_PLAY_SOUND: { LPlaySound(Arg1, true); break; } case ACTION_EXECUTE: { FilterScribeDom dom(App); dom.Email = m; dom.Fil = F; LAutoString cmd(ScribeInsertFields(Arg1, &dom)); if (cmd) { const char *s = cmd; LAutoString exe(LTokStr(s)); LExecute(exe, s); } break; } case ACTION_OPEN: { m->DoUI(); break; } case ACTION_MARK: { if (Stricmp(Arg1.Get(), "false") == 0) { // unmark the item... m->SetMarkColour(0); } else { // parse out RGB auto T = Arg1.SplitDelimit(","); if (T.Length() == 3) { // we have an RGB, so set it baby uint32_t c = Rgb32(atoi(T[0]), atoi(T[1]), atoi(T[2])); m->SetMarkColour(c); } else { uint32_t c = Rgb32(0, 0, 255); m->SetMarkColour(c); } } break; } case ACTION_REPLY: { if (Arg1) { const char *s = Arg1; char *TemplateMsgId = LTokStr(s); SkipSep(s); char *ReplyAll = LTokStr(s); SkipSep(s); char *MarkReplied = LTokStr(s); Mail *Template = GetTemplateMail(App, TemplateMsgId); if (Template) { Thing *t = App->CreateThingOfType(MAGIC_MAIL); if (t) { Mail *n = t->IsMail(); if (n) { bool MarkOriginal = MarkReplied && atoi(MarkReplied); // Prepare mail... n->OnReply(m, ValidStr(ReplyAll)?atoi(ReplyAll)!=0:false, MarkOriginal); n->SetFlags(m->GetFlags() | MAIL_READY_TO_SEND); if (ValidStr(Template->GetSubject())) { n->SetSubject(Template->GetSubject()); } n->SetBody(ScribeInsertFields(Template->GetBody(), F)); // Save it... n->Save(0); // Send it... if not offline. LVariant Offline; App->GetOptions()->GetValue(OPT_WorkOffline, Offline); if (!Offline.CastInt32()) { App->PostEvent(M_COMMAND, IDM_SEND_MAIL, 0); } } else DeleteObj(t); } } DeleteArray(TemplateMsgId); DeleteArray(ReplyAll); DeleteArray(MarkReplied); } break; } case ACTION_FORWARD: { const char *s = Arg1; char *TemplateMsgId = LTokStr(s); SkipSep(s); char *Email = LTokStr(s); SkipSep(s); char *Attach = LTokStr(s); SkipSep(s); char *MarkForwarded = LTokStr(s); bool Attachments = Attach ? atoi(Attach)!=0 : true; if (ValidStr(Email)) { Thing *t = App->CreateThingOfType(MAGIC_MAIL); if (t) { Mail *n = t->IsMail(); if (n) { bool MarkOriginal = MarkForwarded && atoi(MarkForwarded); // Setup email... Mail *Template = TemplateMsgId ? GetTemplateMail(App, TemplateMsgId) : 0; if (Template) { n->SetSubject(Template->GetSubject()); if (ValidStr(Template->GetBody())) { n->SetBody(ScribeInsertFields(Template->GetBody(), F)); } if (Attachments) { List Att; m->GetAttachments(&Att); for (auto a: Att) { n->AttachFile(new Attachment(App, a)); } } } else { n->OnForward(m, MarkOriginal, Attachments); } n->SetFlags(m->GetFlags() | MAIL_READY_TO_SEND); LDataPropI *Addr = n->GetTo()->Create(n->GetObject()->GetStore()); if (Addr) { LVariant v; if (F->GetValue(Email, v)) { Addr->SetStr(FIELD_EMAIL, v.Str()); } else { Addr->SetStr(FIELD_EMAIL, Email); } n->GetTo()->Insert(Addr); } // Save it... n->Save(0); // Send it... if not offline. LVariant Offline; App->GetOptions()->GetValue(OPT_WorkOffline, Offline); if (!Offline.CastInt32()) { App->PostEvent(M_COMMAND, IDM_SEND_MAIL, 0); } } else DeleteObj(t); } } DeleteArray(Email); DeleteArray(Attach); DeleteArray(MarkForwarded); break; } case ACTION_BOUNCE: { const char *s = Arg1; char *Email = LTokStr(s); SkipSep(s); char *Attach = LTokStr(s); SkipSep(s); char *Mark = LTokStr(s); if (ValidStr(Email)) { Thing *t = App->CreateThingOfType(MAGIC_MAIL); if (t) { Mail *n = t->IsMail(); if (n) { bool Attachments = Attach && atoi(Attach); bool MarkOriginal = Mark && atoi(Mark); // Setup email... n->OnBounce(m, MarkOriginal, Attachments); n->SetFlags(m->GetFlags() | MAIL_READY_TO_SEND); LDataPropI *Addr = n->GetTo()->Create(n->GetObject()->GetStore()); if (Addr) { LVariant v; if (F->GetValue(Email, v)) { Addr->SetStr(FIELD_EMAIL, v.Str()); } else { Addr->SetStr(FIELD_EMAIL, Email); } n->GetTo()->Insert(Addr); } // Save it... n->Save(0); // Send it... if not offline. LVariant Offline; App->GetOptions()->GetValue(OPT_WorkOffline, Offline); if (!Offline.CastInt32()) { App->PostEvent(M_COMMAND, IDM_SEND_MAIL, 0); } } else DeleteObj(t); } } DeleteArray(Email); DeleteArray(Attach); DeleteArray(Mark); break; } case ACTION_SAVE_ATTACHMENTS: { const char *arg = Arg1; char *Dir = LTokStr(arg); SkipSep(arg); char *Types = LTokStr(arg); List Files; if (CollectAttachmentsByPattern(m, Types, Files)) { for (auto a: Files) { auto d = GetFileName(a->GetName()); if (d) { char Path[256]; LMakePath(Path, sizeof(Path), Dir, d); a->SaveTo(Path); } } } DeleteArray(Dir); DeleteArray(Types); break; } case ACTION_DELETE_ATTACHMENTS: { List Files; if (CollectAttachmentsByPattern(m, Arg1, Files)) { for (auto a: Files) { m->DeleteAttachment(a); } } break; } case ACTION_CHANGE_CHARSET: { m->SetBodyCharset(Arg1); break; } } return Status; } int CsCmp(LCharset **a, LCharset **b) { return _stricmp((*a)->Charset, (*b)->Charset); } void FilterAction::DescribeHtml(Filter *Flt, LStream &s) { s.Print("%s ", GetText(0)); switch (Type) { case ACTION_MOVE_TO_FOLDER: { ScribeFolder *Folder = Flt->App->GetFolder(Arg1); if (Folder) s.Print("\"%s\"\n", Arg1.Get()); else s.Print("\"%s\"\n", Arg1.Get()); break; } case ACTION_DELETE: break; case ACTION_PRINT: break; case ACTION_PLAY_SOUND: break; case ACTION_OPEN: break; case ACTION_EXECUTE: break; case ACTION_MARK: break; case ACTION_SET_READ: break; case ACTION_LABEL: break; case ACTION_EMPTY_FOLDER: break; case ACTION_MARK_AS_SPAM: break; case ACTION_REPLY: break; case ACTION_FORWARD: break; case ACTION_BOUNCE: break; case ACTION_SAVE_ATTACHMENTS: break; case ACTION_DELETE_ATTACHMENTS: break; case ACTION_CHANGE_CHARSET: break; case ACTION_COPY: break; case ACTION_EXPORT: break; } } void FilterAction::Browse(ScribeWnd *App, LView *Parent) { if (!Parent) return; switch (Type) { default: LAssert(0); break; case ACTION_MOVE_TO_FOLDER: case ACTION_COPY: case ACTION_EMPTY_FOLDER: { auto Dlg = new FolderDlg(Parent, App, MAGIC_MAIL); Dlg->DoModal([this, Dlg](auto dlg, auto id) { if (id) Arg1 = Dlg->Get(); delete dlg; }); break; } case ACTION_EXPORT: { auto s = new LFileSelect(Parent); s->OpenFolder([this](auto s, auto status) { if (status) Arg1 = s->Name(); delete s; }); break; } case ACTION_DELETE: { auto RClick = new LSubMenu; if (RClick) { RClick->AppendItem("Local (default)", IDM_LOCAL, true); RClick->AppendItem("From Server", IDM_SERVER, true); RClick->AppendItem("Local and from Server", IDM_LOCAL_AND_SERVER, true); LMouse m; if (Parent->GetMouse(m, true)) { switch (RClick->Float(Parent, m.x, m.y)) { case IDM_LOCAL: { Arg1 = "local"; break; } case IDM_SERVER: { Arg1 = "server"; break; } case IDM_LOCAL_AND_SERVER: { Arg1 = "local,server"; break; } } } DeleteObj(RClick); } break; } case ACTION_OPEN: { // no configuration break; } case ACTION_SET_READ: { auto RClick = new LSubMenu; if (RClick) { RClick->AppendItem("Read", IDM_TRUE, true); RClick->AppendItem("Unread", IDM_FALSE, true); RClick->AppendItem("Unread But Not New", IDM_NOTNEW, true); LMouse m; if (Parent->GetMouse(m, true)) { switch (RClick->Float(Parent, m.x, m.y)) { case IDM_TRUE: { Arg1 = "true"; break; } case IDM_FALSE: { Arg1 = "false"; break; } case IDM_NOTNEW: { Arg1 = "false,notnew"; break; } } } DeleteObj(RClick); } break; } case ACTION_MARK: { auto RClick = new LSubMenu; if (RClick) { BuildMarkMenu(RClick, MS_One, 0); LMouse m; if (Parent->GetMouse(m, true)) { int Result = RClick->Float(Parent, m.x, m.y); if (Result == IDM_UNMARK) { Arg1 = "False"; } else if (Result >= IDM_MARK_BASE) { char s[32]; sprintf_s(s, sizeof(s), "%i,%i,%i", R32(MarkColours32[Result-IDM_MARK_BASE]), G32(MarkColours32[Result-IDM_MARK_BASE]), B32(MarkColours32[Result-IDM_MARK_BASE])); Arg1 = s; } } } break; } case ACTION_PRINT: { LPrinter Info; #ifdef _MSC_VER #pragma message ("Warning: ACTION_PRINT not implemented.") #endif /* Info.Serialize(Arg1, false); if (Info.Browse(Parent)) { Info.Serialize(Arg1, true); } */ break; } case ACTION_PLAY_SOUND: case ACTION_EXECUTE: { auto Select = new LFileSelect(Parent); Select->Parent(Parent); if (Type == ACTION_PLAY_SOUND) { Select->Type("Wave files", "*.wav"); } else { Select->Type("Executables", "*.exe"); Select->Type("All Files", LGI_ALL_FILES); } Select->Name(Arg1); Select->Open([this](auto s, auto ok) { if (ok) Arg1 = s->Name(); delete s; }); break; } case ACTION_REPLY: { auto Dlg = new BrowseReply(App, Parent, Arg1); Dlg->DoModal([this, Dlg](auto dlg, auto id) { if (id) Arg1 = Dlg->Arg; delete dlg; }); break; } case ACTION_FORWARD: { auto Dlg = new BrowseForward(App, Parent, Arg1, true); Dlg->DoModal([this, Dlg](auto dlg, auto id) { if (id) Arg1 = Dlg->Arg; delete dlg; }); break; } case ACTION_BOUNCE: { auto Dlg = new BrowseForward(App, Parent, Arg1, false); Dlg->DoModal([this, Dlg](auto dlg, auto id) { if (id) Arg1 = Dlg->Arg; delete dlg; }); break; } case ACTION_SAVE_ATTACHMENTS: { auto Dlg = new BrowseSaveAttach(App, Parent, Arg1); Dlg->DoModal([this, Dlg](auto dlg, auto id) { if (id) Arg1 = Dlg->Arg; delete dlg; }); break; } case ACTION_CHANGE_CHARSET: { auto s = new LSubMenu; if (s) { LArray Cs; for (LCharset *c = LGetCsList(); c->Charset; c++) { Cs.Add(c); } Cs.Sort(CsCmp); for (unsigned i=0; iCharset[0] != Cs[i]->Charset[0]) break; n++; } if (n > 1) { char a[64]; char *One = LSeekUtf8(Cs[i]->Charset, 1); ssize_t Len = One - Cs[i]->Charset; memcpy(a, Cs[i]->Charset, Len); strcpy_s(a + Len, sizeof(a) - Len, "..."); auto Sub = s->AppendSub(a); if (Sub) { for (unsigned k=0; kAppendItem(Cs[i+k]->Charset, i+k+1, Cs[i+k]->IsAvailable()); } i += n - 1; } } else { s->AppendItem(Cs[i]->Charset, i+1, Cs[i]->IsAvailable()); } } LMouse m; Parent->GetMouse(m, true); int Result = s->Float(Parent, m.x, m.y, true); if (Result) { Result--; if (Result >= 0 && Result < (int)Cs.Length()) { Arg1 = Cs[Result]->Charset; } } DeleteObj(s); } break; } } } /////////////////////////////////////////////////////////////// int Filter::MaxIndex = -1; Filter::Filter(ScribeWnd *window, LDataI *object) : Thing(window, object) { DefaultObject(object); d = new FilterPrivate; Ui = 0; Current = 0; IgnoreCheckEvents = true; ChkIncoming = new LListItemCheckBox(this, 2, GetIncoming()!=0); ChkOutgoing = new LListItemCheckBox(this, 3, GetOutgoing()!=0); ChkInternal = new LListItemCheckBox(this, 4, GetInternal()!=0); IgnoreCheckEvents = false; } Filter::~Filter() { Empty(); DeleteObj(d); } bool Filter::GetFormats(bool Export, LString::Array &MimeTypes) { MimeTypes.Add(sTextXml); return MimeTypes.Length() > 0; } char *Filter::GetDropFileName() { if (!DropFileName) { auto Nm = GetName(); char f[256]; if (Nm) { LUtf8Ptr o(f); for (LUtf8Ptr i(Nm); (uint32_t)i; i++) { if (!strchr(LGI_IllegalFileNameChars, (uint32_t)i)) o.Add(i); } o.Add(0); } else strcpy_s(f, sizeof(f), "Filter"); strcat_s(f, sizeof(f), ".xml"); DropFileName.Reset(NewStr(f)); } return DropFileName; } bool Filter::GetDropFiles(LString::Array &Files) { char Tmp[MAX_PATH_LEN]; LMakePath(Tmp, sizeof(Tmp), ScribeTempPath(), GetDropFileName()); LAutoPtr Out(new LFile); if (!Out->Open(Tmp, O_WRITE)) return false; if (!Export(AutoCast(Out), sTextXml)) return false; Files.Add(Tmp); return true; } Thing::IoProgress Filter::Import(IoProgressImplArgs) { if (Stricmp(mimeType, sTextXml) && Stricmp(mimeType, sMimeXml)) IoProgressNotImpl(); LXmlTree Tree; LXmlTag r; if (!Tree.Read(&r, stream)) IoProgressError("Xml parse error."); if (!r.IsTag("Filter")) IoProgressError("No filter tag."); Empty(); LXmlTag *t = r.GetChildTag("Name"); if (t && t->GetContent()) SetName(t->GetContent()); SetIndex(r.GetAsInt("index")); if ((t = r.GetChildTag(ELEMENT_CONDITIONS))) { LStringPipe p; if (Tree.Write(t, &p)) { LAutoString s(p.NewStr()); ConditionsCache.Reset(); SetConditionsXml(s); } if ((t = r.GetChildTag("Actions"))) { LStringPipe p; if (Tree.Write(t, &p)) { LAutoString s(p.NewStr()); SetActionsXml(s); } else IoProgressError("Xml write failed."); } } IoProgressSuccess(); } Thing::IoProgress Filter::Export(IoProgressImplArgs) { if (Stricmp(mimeType, sMimeXml)) IoProgressNotImpl(); LXmlTag r("Filter"); LXmlTag *t; if ((t = r.CreateTag("Name"))) t->SetContent(GetName()); r.SetAttr("index", GetIndex()); LAutoPtr Cond = Parse(false); r.InsertTag(Cond.Release()); LAutoPtr Act = Parse(true); r.InsertTag(Act.Release()); LXmlTree tree; if (!tree.Write(&r, stream)) IoProgressError("Failed to write xml."); IoProgressSuccess(); } /// This filters a list of email. The email will have it's NewEmail state set to /// one of three things: /// If the email is not filtered then: /// Mail::NewEmailBayes /// If the email is filtered but not set to !NEW then: /// Mail::NewEmailGrowl /// If the email is filtered AND set to !NEW then: /// Mail::NewMailNone int Filter::ApplyFilters(LView *Parent, List &Filters, List &Email) { int Status = 0; ScribeWnd *App = Filters.Length() > 0 ? Filters[0]->App : NULL; if (!App) return 0; bool Logging = App->LogFilterActivity(); LStream *LogStream = NULL; if (Logging) LogStream = App->ShowScriptingConsole(); LAutoPtr Prog; if (Parent && Prog.Reset(new LProgressDlg(Parent))) { Prog->SetRange(Email.Length()); Prog->SetDescription("Filtering..."); Prog->SetType("email"); } for (auto m: Email) { bool Act = false; bool Stop = false; m->IncRef(); for (auto f: Filters) { if (Stop) break; if (f->Test(m, Stop, LogStream)) { f->DoActions(m, Stop, LogStream); Act = true; } } if (Act) { Status++; if (m && m->NewEmail == Mail::NewEmailFilter) { m->NewEmail = Mail::NewEmailGrowl; } } else if (m && m->NewEmail == Mail::NewEmailFilter) { m->NewEmail = Mail::NewEmailBayes; } m->DecRef(); m = NULL; if (Prog) { Prog->Value(Prog->Value() + 1); if (Prog->IsCancelled()) break; } } return Status; } Filter *Filter::GetFilterAt(int Index) { ScribeFolder *f = GetFolder(); if (f) { for (auto t: f->Items) { Filter *f = t->IsFilter(); if (f && f->GetIndex() == Index) { return f; } } } return 0; } enum TermType { TermString, TermVariant }; class ExpTerm { public: TermType Type; LVariant Value; ExpTerm(TermType t) { Type = t; } }; bool Filter::Evaluate(char *str, LVariant &v) { char *BufStr = NewStr(str); char *s = BufStr; if (s) { List Terms; const char *Ws = " \t\r\n"; while (s && *s) { while (*s && strchr(Ws, *s)) s++; if (*s && *s == '\"') { char *Start = ++s; char *In = s; char *Out = s; while (*In) { if (In[0] == '\"') { if (In[1] == '\"') { // Quote *Out++ = '\"'; In += 2; } else { // End of string In++; break; } } else { *Out++ = *In++; } } *Out++ = 0; s = In; ExpTerm *t; Terms.Insert(t = new ExpTerm(TermString)); if (t) { t->Value = Start; } } else { char *Start = s; while (*s && !strchr(Ws, *s)) s++; while (*s && strchr(Ws, *s)) s++; char *Src = NewStr(Start, s-Start); if (Src) { ExpTerm *t; Terms.Insert(t = new ExpTerm(TermVariant)); if (t) { if (GetValue(Start, t->Value)) { switch (t->Value.Type) { default: break; case GV_BINARY: case GV_LIST: case GV_DOM: case GV_VOID_PTR: { v = t->Value; DeleteArray(BufStr); DeleteArray(Src); Terms.DeleteObjects(); return true; } } } else { t->Type = TermString; t->Value = Src; } } DeleteArray(Src); } } } LStringPipe Out; auto It = Terms.begin(); ExpTerm *t = *It; if (t) { if (Terms.Length() > 1) { // Collapse terms for (; t; t=*(++It)) { char Buf[128]; switch (t->Value.Type) { default: break; case GV_INT32: { sprintf_s(Buf, sizeof(Buf), "%i", t->Value.Value.Int); Out.Push(Buf); break; } case GV_INT64: { sprintf_s(Buf, sizeof(Buf), LPrintfInt64, t->Value.Value.Int64); Out.Push(Buf); break; } case GV_BOOL: { sprintf_s(Buf, sizeof(Buf), "%i", (int)t->Value.Value.Bool); Out.Push(Buf); break; } case GV_DOUBLE: { sprintf_s(Buf, sizeof(Buf), "%g", t->Value.Value.Dbl); Out.Push(Buf); break; } case GV_STRING: { if (t->Value.Str()) Out.Push(t->Value.Str()); break; } case GV_DATETIME: { t->Value.Value.Date->Get(Buf, sizeof(Buf)); Out.Push(Buf); break; } case GV_BINARY: case GV_LIST: case GV_DOM: case GV_NULL: case GV_VOID_PTR: { break; } } } v.OwnStr(Out.NewStr()); } else { v = t->Value; } } Terms.DeleteObjects(); } DeleteArray(BufStr); return true; } bool Filter::SetVariant(const char *Name, LVariant &Value, const char *Array) { ScribeDomType Field = StrToDom(Name); switch (Field) { case SdName: SetName(Value.Str()); break; case SdConditionsXml: ConditionsCache.Reset(); SetConditionsXml(Value.Str()); break; case SdActionsXml: SetActionsXml(Value.Str()); break; case SdDateModified: return SetDateField(FIELD_DATE_MODIFIED, Value); default: return false; } return true; } bool Filter::CallMethod(const char *MethodName, LVariant *ReturnValue, LArray &Args) { ScribeDomType Method = StrToDom(MethodName); switch (Method) { case SdAddCondition: // Type: (String Feild, String Op, String Value) { if (Args.Length() != 3) { LgiTrace("%s:%i - SdAddCondition: wrong number of parameters %i (expecting 3)\n", _FL, Args.Length()); break; } const char *Field = Args[0]->Str(); const char *Op = Args[1]->Str(); const char *Value = Args[2]->Str(); if (!Field || !Op || !Value) { LgiTrace("%s:%i - SdAddCondition: Missing values.\n", _FL); break; } LAutoPtr t = Parse(false); LXmlTag *Cond; if (!t || !t->IsTag(ELEMENT_CONDITIONS) || (Cond = t->Children[0]) == NULL) { LgiTrace("%s:%i - SdAddCondition: Failed to parse conditions.\n", _FL); break; } if (!Cond->IsTag(ELEMENT_AND) && !Cond->IsTag(ELEMENT_OR)) { LgiTrace("%s:%i - SdAddCondition: Unexpected root operator.\n", _FL); break; } // Check that the condition doesn't already exist... for (auto c : Cond->Children) { if (c->IsTag(ELEMENT_CONDITION)) { char *CFeild = c->GetAttr(ATTR_FIELD); char *COp = c->GetAttr(ATTR_OP); char *CVal = c->GetAttr(ATTR_VALUE); if (!Stricmp(Field, CFeild) && !Stricmp(Op, COp) && !Stricmp(Value, CVal)) { return true; } } } LXmlTag *n = new LXmlTag(ELEMENT_CONDITION); if (!n) { LgiTrace("%s:%i - SdAddCondition: Alloc failed.\n", _FL); break; } n->SetAttr(ATTR_FIELD, Field); n->SetAttr(ATTR_OP, Op); n->SetAttr(ATTR_VALUE, Value); Cond->InsertTag(n); LXmlTree tree; LStringPipe p; if (!tree.Write(t, &p)) { LgiTrace("%s:%i - SdAddCondition: Failed to write XML.\n", _FL); break; } LAutoString a(p.NewStr()); ConditionsCache.Reset(); SetConditionsXml(a); SetDirty(true); return true; } case SdAddAction: // Type: (String ActionName, String Value) { if (Args.Length() != 2) { LgiTrace("%s:%i - SdAddAction: wrong number of parameters %i (expecting 3)\n", _FL, Args.Length()); break; } LString Action = Args[0]->CastString(); if (!Action) { LgiTrace("%s:%i - SdAddAction: Missing action name.\n", _FL); break; } FilterAction a(GetObject()->GetStore()); for (ActionName *an = ActionNames; an->Id; an++) { if (Action.Equals(an->Default)) { a.Type = (FilterActionTypes) (an - ActionNames); a.Arg1 = Args[1]->CastString(); AddAction(&a); return true; } } LgiTrace("%s:%i - SdAddAction: Action '%s' not found.\n", _FL, Action.Get()); return false; } case SdStopFiltering: // Type: () { if (d->Stop) { *d->Stop = true; return true; } else LgiTrace("%s:%i - No stop parameter to set.\n", _FL); return true; } case SdDoActions: // Type: (Mail Object) { if (Args.Length() == 1) { LDom *d = Args[0]->CastDom(); Mail *m = dynamic_cast(d); if (m) { bool Stop = false; return DoActions(m, Stop); } else LgiTrace("%s:%i - DoActions: failed to cast arg1 to Mail object.\n", _FL); } else LgiTrace("%s:%i - DoActions is expecting 1 argument, not %i.\n", _FL, Args.Length()); return true; } default: break; } return Thing::CallMethod(MethodName, ReturnValue, Args); } bool Filter::GetVariant(const char *Var, LVariant &Value, const char *Array) { ScribeDomType Fld = StrToDom(Var); switch (Fld) { case SdMail: // Type: Mail { if (!Current) return false; Value = *Current; break; } case SdScribe: // Type: ScribeWnd { Value = (LDom*)App; break; } case SdName: // Type: String { Value = GetName(); break; } case SdTestConditions: // Type: Bool { if (Current && *Current) { bool s; bool &Stop = d->Stop ? *d->Stop : s; Value = EvaluateXml(*Current, Stop, d->Log); } else return false; break; } case SdType: // Type: Int32 { Value = GetObject()->Type(); break; } case SdConditionsXml: // Type: String { Value = GetConditionsXml(); break; } case SdActionsXml: // Type: String { Value = GetActionsXml(); break; } case SdIndex: // Type: Int32 { Value = GetIndex(); break; } case SdDateModified: // Type: DateTime { return GetDateField(FIELD_DATE_MODIFIED, Value); } default: { return false; } } return true; } Thing &Filter::operator =(Thing &t) { Filter *f = t.IsFilter(); if (f) { if (GetObject() && f->GetObject()) { GetObject()->CopyProps(*f->GetObject()); } } return *this; } int Filter::Compare(LListItem *Arg, ssize_t Field) { Filter *a = dynamic_cast(Arg); if (a) { switch (Field) { case FIELD_FILTER_NAME: return a && GetName() ? _stricmp(GetName(), a->GetName()) : -1; case FIELD_FILTER_INDEX: return GetIndex() - a->GetIndex(); case FIELD_FILTER_INCOMING: return GetIncoming() - a->GetIncoming(); case FIELD_FILTER_OUTGOING: return GetOutgoing() - a->GetOutgoing(); } } return 0; } int Filter::GetImage(int Flags) { return ICON_FILTER; } void DisplayXml(LStream &s, char *xml) { char *start = xml; char *e; for (e = xml; *e; e++) { if (*e == '<') { s.Write(start, e - start); s.Print("<"); start = e + 1; } } s.Write(start, e - start); } void DescribeCondition(LStream &s, LXmlTag *t) { int i = 0; if (t->IsTag(ELEMENT_AND) || t->IsTag(ELEMENT_OR)) { for (auto c: t->Children) { LStringPipe p; DescribeCondition(p, c); LAutoString a(p.NewStr()); if (i) s.Print(" %s ", t->GetTag()); s.Print("(%s)", a.Get()); i++; } } else { // Condition char *f = t->GetAttr(ATTR_FIELD); char *v = t->GetAttr(ATTR_VALUE); int Not = t->GetAsInt(ATTR_NOT) > 0; const char *o = t->GetAttr(ATTR_OP); if (o && IsDigit(*o)) o = OpNames[atoi(o)]; s.Print("%s%s %s \"%s\"", Not ? "!" : "", f, o, v); } } LAutoString Filter::DescribeHtml() { LStringPipe p(256); p.Print("<style>\n" ".error { color: red; font-weight: bold; }\n" ".op { color: blue; }\n" ".var { color: #800; }\n" "pre { color: green; }\n" "</style>\n" "\n" "Name: %s
\n", GetName()); p.Print("Incoming: %i
\n", GetIncoming()); p.Print("Outgoing: %i
\n", GetOutgoing()); if (GetConditionsXml()) { LAutoPtr r = Parse(false); if (r && r->Children.Length()) { p.Print("Conditions:
    \n"); for (auto c: r->Children) { p.Print("
  • "); DescribeCondition(p, c); } p.Print("
\n"); } } if (GetActionsXml()) { LAutoPtr r = Parse(true); if (r && r->Children.Length()) { p.Print("Actions:
    \n"); for (auto c: r->Children) { if (!c->IsTag(ELEMENT_ACTION)) continue; LAutoPtr a(new FilterAction(GetObject()->GetStore())); if (a->Set(c)) { p.Print("
  • "); a->DescribeHtml(this, p); } } p.Print("
\n"); } } if (GetScript()) { LXmlTree t; LAutoString e(t.EncodeEntities(GetScript(), -1, "<>")); p.Print("Script:
%s
\n", e.Get()); } p.Print("\n"); return LAutoString(p.NewStr()); } void Filter::Empty() { if (GetObject()) { ConditionsCache.Reset(); SetConditionsXml(0); SetName(0); } } bool Filter::EvaluateTree(LXmlTag *n, Mail *m, bool &Stop, LStream *Log) { bool Status = false; if (n && n->GetTag() && m && m->GetObject()) { if (n->IsTag(ELEMENT_AND)) { if (Log) Log->Print("\tAnd {\n"); for (auto c: n->Children) { if (!EvaluateTree(c, m, Stop, Log)) { if (Log) Log->Print("\t} (false)\n"); return false; } } if (Log) Log->Print("\t} (true)\n"); Status = true; } else if (n->IsTag(ELEMENT_OR)) { if (Log) Log->Print("\tOr {\n"); for (auto c: n->Children) { if (EvaluateTree(c, m, Stop, Log)) { if (Log) Log->Print("\t} (true)\n"); return true; } } if (Log) Log->Print("\t} (false)\n"); } else if (n->IsTag(ELEMENT_CONDITION)) { FilterCondition *c = new FilterCondition; if (c) { if (!c->Set(n)) { LAssert(0); } else { Status = c->Test(this, m, Log); if (c->Not) Status = !Status; if (Log) { Log->Print("\tResult=%i (not=%i)\n", Status, c->Not); } } DeleteObj(c); } } else LAssert(0); } else LAssert(0); return Status; } bool FilterCondition::Set(LXmlTag *t) { if (!t) return false; Source.Reset(NewStr(t->GetAttr(ATTR_FIELD))); Value.Reset(NewStr(t->GetAttr(ATTR_VALUE))); Not = t->GetAsInt(ATTR_NOT) > 0; char *o = t->GetAttr(ATTR_OP); if (o) { if (IsDigit(*o)) Op = atoi(o); else { for (int i=0; OpNames[i]; i++) { if (!_stricmp(OpNames[i], o)) { Op = i; break; } } } } return true; } bool Filter::EvaluateXml(Mail *m, bool &Stop, LStream *Log) { bool Status = false; if (ValidStr(GetConditionsXml())) { if (!ConditionsCache) ConditionsCache = Parse(false); if (ConditionsCache && ConditionsCache->Children.Length()) Status = EvaluateTree(ConditionsCache->Children[0], m, Stop, Log); } return Status; } bool Filter::Test(Mail *m, bool &Stop, LStream *Log) { bool Status = false; if (Log) Log->Print("Filter.Test '%s':\n", GetName()); Current = &m; if (m && m->GetObject()) { d->Stop = &Stop; d->Log = Log; if (ValidStr(GetScript())) { OnFilterScript(this, m, GetScript()); } else if (ValidStr(GetConditionsXml())) { Status = EvaluateXml(m, Stop, Log); } else LAssert(0); d->Stop = 0; d->Log = 0; } Current = 0; return Status; } bool Filter::DoActions(Mail *&m, bool &Stop, LStream *Log) { if (!App || !m) return false; Current = &m; LAutoPtr r = Parse(true); if (r) { LArray Act; for (auto c: r->Children) { if (c->IsTag(ELEMENT_ACTION)) { FilterAction *a = new FilterAction(GetObject()->GetStore()); if (a) { if (a->Set(c)) Act.Add(a); else LAssert(!"Can't convert xml to action."); } } } for (unsigned i=0; iGetObject(); i++) { FilterAction *a = Act[i]; a->Do(this, App, m, Log); } Act.DeleteObjects(); } Current = 0; if (GetStopFiltering()) { Stop = true; } return true; } ThingUi *Filter::DoUI(MailContainer *c) { if (!Ui) { MaxIndex = MAX(MaxIndex, GetIndex()); if (GetIndex() < 0) { SetIndex(++MaxIndex); } Ui = new FilterUi(this); } #if WINNATIVE if (Ui) SetForegroundWindow(Ui->Handle()); #endif return Ui; } LAutoPtr Filter::Parse(bool Actions) { LAutoPtr Ret; auto RawXml = Actions ? GetActionsXml() : GetConditionsXml(); if (!RawXml) return Ret; LMemStream Xml(RawXml, strlen(RawXml), false); LXmlTree t; Ret.Reset(new LXmlTag); if (!t.Read(Ret, &Xml)) Ret.Reset(); return Ret; } void Filter::AddAction(FilterAction *Action) { if (!Action) return; LAutoPtr a = Parse(true); if (!a) a.Reset(new LXmlTag("Actions")); LAutoPtr n(new LXmlTag(ELEMENT_ACTION)); if (!Action->Get(n)) return; a->InsertTag(n.Release()); LXmlTree t; LStringPipe p; if (!t.Write(a, &p)) return; LAutoString Xml(p.NewStr()); SetActionsXml(Xml); SetDirty(true); } bool Filter::Save(ScribeFolder *Into) { bool Status = false; if (!Into) { Into = GetFolder(); if (!Into) { Into = App->GetFolder(FOLDER_FILTERS); } } if (Into) { SetParentFolder(Into); if (ChkIncoming) SetIncoming(ChkIncoming->Value()!=0); if (ChkOutgoing) SetOutgoing(ChkOutgoing->Value()!=0); if (ChkInternal) SetInternal(ChkInternal->Value()!=0); LDateTime Now; GetObject()->SetDate(FIELD_DATE_MODIFIED, &Now.SetNow()); Store3Status s = Into->WriteThing(this); Status = s != Store3Error; if (Status) SetDirty(false); } return Status; } void Filter::OnPaint(ItemPaintCtx &Ctx) { LListItem::OnPaint(Ctx); } void Filter::OnMouseClick(LMouse &m) { LListItem::OnMouseClick(m); if (m.Double()) { // open the UI for the Item DoUI(); } else if (m.Right()) { // open the right click menu auto RClick = new LSubMenu; if (RClick) { RClick->AppendItem(LLoadString(IDS_OPEN), IDM_OPEN); RClick->AppendItem(LLoadString(IDS_DELETE), IDM_DELETE); RClick->AppendItem(LLoadString(IDS_EXPORT), IDM_EXPORT); if (Parent->GetMouse(m, true)) { switch (RClick->Float(Parent, m.x, m.y)) { case IDM_OPEN: { DoUI(); break; } case IDM_DELETE: { LVariant ConfirmDelete; App->GetOptions()->GetValue(OPT_ConfirmDelete, ConfirmDelete); if (!ConfirmDelete.CastInt32() || LgiMsg(GetList(), LLoadString(IDS_DELETE_ASK), AppName, MB_YESNO) == IDYES) { List Del; LList *ParentList = LListItem::Parent; if (ParentList && ParentList->GetSelection(Del)) { for (auto m: Del) { auto f = dynamic_cast(m); if (f) f->OnDelete(); } } } break; } case IDM_EXPORT: { ExportAll(GetList(), sTextXml, NULL); break; } } } DeleteObj(RClick); } } } int *Filter::GetDefaultFields() { static int Def[] = { FIELD_FILTER_NAME, 0, 0, 0 }; return Def; } const char *Filter::GetFieldText(int Field) { switch (Field) { case FIELD_FILTER_NAME: return GetName(); case FIELD_FILTER_CONDITIONS_XML: return GetConditionsXml(); case FIELD_FILTER_ACTIONS_XML: return GetActionsXml(); case FIELD_FILTER_SCRIPT: return GetScript(); } return NULL; } void Filter::OnColumnNotify(int Col, int64 Data) { if (!IgnoreCheckEvents) { switch (Col) { case 2: case 3: case 4: { SetDirty(true); break; } } } } const char *Filter::GetText(int i) { int Field = 0; if (FieldArray.Length()) { if (i >= 0 && i < (int)FieldArray.Length()) Field = FieldArray[i]; } else if (i >= 0 && i < CountOf(DefaultFilterFields)) { Field = DefaultFilterFields[i]; } switch (Field) { case FIELD_FILTER_NAME: { return GetName(); break; } case FIELD_FILTER_INDEX: { static char i[16]; sprintf_s(i, sizeof(i), "%i", GetIndex()); return i; break; } } return 0; } int FilterIndexCmp(Filter **a, Filter **b) { int ai = (*a)->GetIndex(); int bi = (*b)->GetIndex(); return ai - bi; } void Filter::Reindex(ScribeFolder *Folder) { if (!Folder) return; LArray Zero; LArray Sort; for (auto t : Folder->Items) { Filter *f = t->IsFilter(); if (f) { int Idx = f->GetIndex(); if (Idx > 0) { Sort.Add(f); } else { Zero.Add(f); } } } Sort.Sort(FilterIndexCmp); LArray Map; for (unsigned n=0; nGetIndex(); if (Idx != i + 1) { Sort[i]->SetIndex(i + 1); Sort[i]->SetDirty(); Sort[i]->Update(); Changed = true; } } if (Changed) Folder->ReSort(); } ////////////////////////////////////////////////////////// enum ConditionOptions { FIELD_ANYWHERE = FIELD_MAX }; int FilterCallback( LFilterView *View, LFilterItem *Item, GFilterMenu Menu, LRect &r, LArray *GetList, void *Data) { int Status = -1; if (!View) return Status; switch (Menu) { case FMENU_FIELD: { LSubMenu s; LString::Array Names; ItemFieldDef *FieldDefs = MailFieldDefs; for (ItemFieldDef *i = FieldDefs; i->DisplayText; i++) { const char *Trans = LLoadString(i->FieldId); auto idx = (i - FieldDefs) + 1; Names[idx] = DomToStr(i->Dom); s.AppendItem(Trans ? Trans : i->DisplayText, (int)idx, true); } s.AppendItem(LLoadString(IDS_ATTACHMENTS_DATA), FIELD_ATTACHMENTS_DATA, true); s.AppendItem(LLoadString(IDS_ATTACHMENTS_NAME), FIELD_ATTACHMENTS_NAME, true); s.AppendItem(LLoadString(IDS_MEMBER_OF_GROUP), FIELD_MEMBER_OF_GROUP, true); s.AppendItem(LLoadString(IDS_ANYWHERE), FIELD_ANYWHERE, true); LPoint p(r.x1, r.y2 + 1); View->PointToScreen(p); int Cmd = s.Float(View, p.x, p.y, true); switch (Cmd) { case FIELD_ATTACHMENTS_DATA: Item->SetField("mail.Attachments"); break; case FIELD_ATTACHMENTS_NAME: Item->SetField("mail.AttachmentNames"); break; case FIELD_MEMBER_OF_GROUP: Item->SetField("mail.From.Groups"); break; case FIELD_ANYWHERE: Item->SetField("mail.*"); break; default: if (Cmd > 0 && Cmd < Names.Length()) Item->SetField(LString("mail.") + Names[Cmd]); break; } break; } case FMENU_OP: { if (GetList) { for (const char **o = GetOpNames(true); *o; o++) { GetList->Add(NewStr(*o)); } Status = true; } /* else { auto s = new LSubMenu; if (s) { int n = 1; for (char **o = GetOpNames(true); *o; o++) { s->AppendItem(*o, n++, true); } LPoint p(r.x1, r.y2 + 1); View->PointToScreen(p); int Cmd = s->Float(View, p.x, p.y, true); if (Cmd > 0) { Item->SetOp(OpNames[Cmd - 1]); } DeleteObj(s); } } */ break; } case FMENU_VALUE: { break; } } return Status; } ////////////////////////////////////////////////////////// struct FilterUiPriv { }; FilterUi::FilterUi(Filter *item) : ThingUi(item, "Filter") { d = new FilterUiPriv; Item = item; if (!(Item && Item->App)) { return; } Script = 0; Tab = 0; Actions = 0; Conditions = 0; LRect r(100, 100, 800, 600); SetPos(r); MoveSameScreen(item->App); // Create window #if WINNATIVE CreateClassW32("FilterUi", LoadIcon(LProcessInst(), MAKEINTRESOURCE(IDI_FILTER))); #endif if (Attach(0)) { // Setup UI Commands.Toolbar = Item->App->LoadToolbar(this, Item->App->GetResourceFile(ResToolbarFile), Item->App->GetToolbarImgList()); if (Commands.Toolbar) { Commands.Toolbar->Attach(this); Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_SAVE)), IDM_SAVE, TBT_PUSH, true, IMG_SAVE); Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_SAVE_CLOSE)), IDM_SAVE_CLOSE, TBT_PUSH, true, IMG_SAVE_AND_CLOSE); Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_DELETE)), IDM_DELETE, TBT_PUSH, true, IMG_TRASH); Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_HELP)), IDM_HELP, TBT_PUSH, true, IMG_HELP); Commands.SetupCallbacks(GetItem()->App, this, GetItem(), LThingUiToolbar); } LTabPage *Cond = 0; LTabPage *Act = 0; Tab = new LTabView(91, 0, 0, 1000, 1000, 0); if (Tab) { Tab->Attach(this); Tab->SetPourChildren(true); LTabPage *Filter = Tab->Append(LLoadString(IDS_FILTER)); if (Filter) { #ifdef _DEBUG auto Status = #endif Filter->LoadFromResource(IDD_FILTER); LAssert(Status); Name(Filter->Name()); } Cond = Tab->Append(LLoadString(IDS_CONDITIONS)); if (Cond) { Conditions = new LFilterView(FilterCallback, Item); if (Conditions) { Cond->Append(Conditions); Conditions->SetPourLargest(true); } } Act = Tab->Append(LLoadString(IDS_ACTIONS)); if (Act) { #ifdef _DEBUG auto Status = #endif Act->LoadFromResource(IDD_FILTER_ACTION); LAssert(Status); if (GetViewById(IDC_FILTER_ACTIONS, Actions)) Actions->MultiSelect(false); } LTabPage *ScriptTab = Tab->Append(""); if (ScriptTab && ScriptTab->LoadFromResource(IDD_FILTER_SCRIPT)) { if (GetViewById(IDC_SCRIPT, Script)) { Script->SetWrapType(TEXTED_WRAP_NONE); Script->Sunken(true); Script->SetPourLargest(true); } else LAssert(0); } } // Show window Visible(true); if (Cond && Item) { LCombo *Cbo; if (GetViewById(IDC_ACTION, Cbo)) { for (ActionName *o = ActionNames; o->Id; o++) { const char *s = LLoadString(o->Id, o->Default); Cbo->Insert(s); } } } OnLoad(); } RegisterHook(this, LKeyEvents); } FilterUi::~FilterUi() { if (Item) Item->Ui = 0; DeleteObj(d); } bool FilterUi::OnViewKey(LView *v, LKey &k) { if (k.CtrlCmd()) { switch (k.c16) { case 's': case 'S': { if (k.Down()) OnSave(); return true; } case 'w': case 'W': { if (k.Down()) { OnSave(); Quit(); } return true; } } } return false; } int FilterUi::OnNotify(LViewI *Col, LNotification n) { int Reindex = 0; int InsertOffset = 0; switch (Col->GetId()) { case IDC_TYPE_CBO: case IDC_ARG_EDIT: { if (Actions) { List Sel; if (Actions->GetSelection(Sel)) { for (auto a: Sel) a->OnNotify(Col, n); } } break; } case IDC_BROWSE_ARG: { if (Actions) { List Sel; if (Actions->GetSelection(Sel)) { FilterAction *a = Sel[0]; if (a) { a->Browse(Item->App, this); a->OnNotify(Col, n); } } } break; } case IDC_UP: InsertOffset = -1; // fall thru case IDC_DOWN: { if (!InsertOffset) InsertOffset = 1; if (!Actions) break; List Items; if (!Actions->GetSelection(Items) && Items[0]) break; int Idx = Actions->IndexOf(Items[0]) + InsertOffset; FilterAction *Last = 0; for (auto a: Items) { Actions->Remove(a); Actions->Insert(a, Idx++); Last = a; } if (Last) { Actions->Focus(true); Last->Select(true); } break; } case IDC_NEW_FILTER_ACTION: { if (Actions) { FilterAction *n = new FilterAction(Item->GetObject()->GetStore()); Actions->Insert(n); Actions->Select(n); Actions->Focus(true); } break; } case IDC_DELETE_FILTER_ACTION: { if (Actions) { List Items; if (Actions->GetSelection(Items) && Items[0]) { int Idx = Actions->IndexOf(Items[0]); Items.DeleteObjects(); if (Idx >= (int)Actions->Length()) Idx = (int)Actions->Length() - 1; Actions->Select(Actions->ItemAt(Idx)); Actions->Focus(true); } } break; } case IDC_LAUNCH_HELP: { switch (Tab->Value()) { case 0: // Name/Index default: { Item->App->LaunchHelp("filters.html"); break; } case 1: // Conditions { Item->App->LaunchHelp("filters.html#cond"); break; } case 2: // Actions { Item->App->LaunchHelp("filters.html#actions"); break; } case 3: // Script { Item->App->LaunchHelp("filters.html#script"); break; } } break; } case IDC_FILTER_UP: { Reindex = 1; break; } case IDC_FILTER_DOWN: { Reindex = -1; break; } } if (Reindex) { // Remove holes in the indexing ScribeFolder *Folder = Item->GetFolder(); if (Folder) { Folder->ReSort(); } // Swap entries int i = (int)GetCtrlValue(IDC_FILTER_INDEX); if (i >= 0) { Filter *f = Item->GetFilterAt(i - Reindex); if (f) { int n = f->GetIndex(); f->SetIndex(Item->GetIndex()); f->SetDirty(); Item->SetIndex(n); Item->SetDirty(); f->Save(); Item->Save(); SetCtrlValue(IDC_FILTER_INDEX, Item->GetIndex()); Item->App->GetItemList()->ReSort(); Item->Update(); f->Update(); } } } return 0; } LMessage::Result FilterUi::OnEvent(LMessage *Msg) { switch (Msg->Msg()) { #ifdef WIN32 case WM_CLOSE: { Quit(); return 0; } #endif } return LWindow::OnEvent(Msg); } void LoadTree(LFilterView *v, LXmlTag *t, LTreeNode *i) { int idx = 0; for (auto c: t->Children) { if (c->GetTag()) { LFilterItem *n = 0; bool Cond = false; if (c->IsTag(ELEMENT_AND)) n = v->Create(LNODE_AND); else if (c->IsTag(ELEMENT_OR)) n = v->Create(LNODE_OR); else if (c->IsTag(ELEMENT_CONDITION)) { Cond = true; n = v->Create(LNODE_COND); } if (n) { if (Cond) { n->SetNot(c->GetAsInt(ATTR_NOT) > 0); n->SetField(c->GetAttr(ATTR_FIELD)); n->SetValue(c->GetAttr(ATTR_VALUE)); char *o = c->GetAttr(ATTR_OP); if (o) { if (IsDigit(*o)) n->SetOp(atoi(o)); else { for (int i=0; OpNames[i]; i++) { if (!_stricmp(OpNames[i], o)) { n->SetOp(i); break; } } } } } i->Insert(n, idx++); LoadTree(v, c, n); } } } } void FilterUi::OnLoad() { if (Item) { LAutoPtr r = Item->Parse(true); if (r && Actions) { for (auto c: r->Children) { FilterAction *a = new FilterAction(Item->GetObject()->GetStore()); if (a) { if (a->Set(c)) { Actions->Insert(a); } else LAssert(!"Can't convert xml to action."); } } } auto FilterName = Item->GetName(); SetCtrlName(IDC_NAME, FilterName); SetCtrlValue(IDC_FILTER_INDEX, Item->GetIndex()); SetCtrlName(IDC_SCRIPT, Item->GetScript()); SetCtrlValue(IDC_STOP_FILTERING, Item->GetStopFiltering()); SetCtrlValue(IDC_INCOMING, Item->GetIncoming()); SetCtrlValue(IDC_OUTGOING, Item->GetOutgoing()); SetCtrlValue(IDC_INTERNAL_FILTERING, Item->GetInternal()); auto Xml = Item->GetConditionsXml(); if (Conditions && Xml) { LAutoPtr x(new LXmlTag); if (x) { LMemStream p(Xml, strlen(Xml)); LXmlTree t; if (t.Read(x, &p, 0)) { Conditions->Empty(); LoadTree(Conditions, x, Conditions->GetRootNode()); if (!Conditions->GetRootNode()->GetChild()) { Conditions->SetDefault(); } } } } if (ValidStr(FilterName)) { LString s; s.Printf("%s - %s", LLoadString(IDS_FILTER), FilterName); Name(s); } } } void SaveTree(LXmlTag *t, LTreeNode *i) { for (LTreeNode *c = i->GetChild(); c; c = c->GetNext()) { LFilterItem *fi = dynamic_cast(c); if (fi) { const char *Tag = 0; bool Cond = false; switch (fi->GetNode()) { default: break; case LNODE_AND: Tag = ELEMENT_AND; break; case LNODE_OR: Tag = ELEMENT_OR; break; case LNODE_COND: Cond = true; Tag = ELEMENT_CONDITION; break; } if (Tag) { LXmlTag *n = new LXmlTag(Tag); if (n) { if (Cond) { n->SetAttr(ATTR_NOT, fi->GetNot()); n->SetAttr(ATTR_FIELD, fi->GetField()); n->SetAttr(ATTR_OP, fi->GetOp()); n->SetAttr(ATTR_VALUE, fi->GetValue()); } t->InsertTag(n); SaveTree(n, fi); } } } } } void FilterUi::OnSave() { if (Item) { Item->SetName(GetCtrlName(IDC_NAME)); Item->SetScript(GetCtrlName(IDC_SCRIPT)); Item->SetStopFiltering(GetCtrlValue(IDC_STOP_FILTERING)!=0); Item->SetIncoming(GetCtrlValue(IDC_INCOMING)!=0); Item->SetOutgoing(GetCtrlValue(IDC_OUTGOING)!=0); Item->ChkIncoming->Value(GetCtrlValue(IDC_INCOMING)); Item->ChkOutgoing->Value(GetCtrlValue(IDC_OUTGOING)); Item->ChkInternal->Value(GetCtrlValue(IDC_INTERNAL_FILTERING)); if (Conditions) { LXmlTag *x = new LXmlTag(ELEMENT_CONDITIONS); if (x) { SaveTree(x, Conditions->GetRootNode()); LXmlTree t; LStringPipe p; if (t.Write(x, &p)) { LAutoString a(p.NewStr()); Item->ConditionsCache.Reset(); Item->SetConditionsXml(a); } DeleteObj(x); } } LXmlTag x("Actions"); List Act; Actions->GetAll(Act); for (size_t i=0; i c(new LXmlTag("Action")); if (a->Get(c)) { x.InsertTag(c.Release()); } } LXmlTree t; LStringPipe p; t.Write(&x, &p); LAutoString s(p.NewStr()); Item->SetActionsXml(s); if (Item->Save()) { Item->Reindex(Item->GetFolder()); } } } int FilterUi::OnCommand(int Cmd, int Event, OsView Window) { switch (Cmd) { case IDM_SAVE: { OnSave(); break; } case IDM_SAVE_CLOSE: { OnSave(); // fall thru } case IDM_CLOSE: { Quit(); break; } case IDM_DELETE: { Item->Ui = 0; Item->OnDelete(); Item = 0; Quit(); break; } case IDM_HELP: { App->LaunchHelp("filters.html"); break; } case IDC_NEW_FILTER_ACTION: { LList *l; if (GetViewById(IDC_FILTER_ACTIONS, l)) { } break; } case IDC_DELETE_FILTER_ACTION: { break; } default: { Commands.ExecuteCallbacks(GetItem()->App, this, GetItem(), Cmd); break; } } return 0; } diff --git a/src/ScribeMail.cpp b/src/ScribeMail.cpp --- a/src/ScribeMail.cpp +++ b/src/ScribeMail.cpp @@ -1,10334 +1,10333 @@ /* ** 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(128, 128, 128) // grey }; ItemFieldDef MailFieldDefs[] = { {"To", SdTo, GV_STRING, FIELD_TO}, {"From", SdFrom, GV_STRING, FIELD_FROM}, {"Subject", SdSubject, GV_STRING, FIELD_SUBJECT}, {"Size", SdSize, GV_INT64, FIELD_SIZE}, {"Received Date", SdReceivedDate, GV_DATETIME, FIELD_DATE_RECEIVED}, {"Send Date", SdSendDate, GV_DATETIME, FIELD_DATE_SENT}, {"Body", SdBody, GV_STRING, FIELD_TEXT}, {"Internet Header", SdInternetHeader, GV_STRING, FIELD_INTERNET_HEADER, IDC_INTERNET_HEADER}, {"Message ID", SdMessageId, GV_STRING, FIELD_MESSAGE_ID}, {"Priority", SdPriority, GV_INT32, FIELD_PRIORITY}, {"Flags", SdFlags, GV_INT32, FIELD_FLAGS}, {"Html", SdHtml, GV_STRING, FIELD_ALTERNATE_HTML}, {"Label", SdLabel, GV_STRING, FIELD_LABEL}, {"From Contact", SdContact, GV_STRING, FIELD_FROM_CONTACT_NAME}, {"File", SdFile, GV_STRING, FIELD_CACHE_FILENAME}, {"ImapFlags", SdImapFlags, GV_STRING, FIELD_CACHE_FLAGS}, {"ImapSeq", SdFile, GV_INT32, FIELD_IMAP_SEQ}, {"ImapUid", SdImapFlags, GV_INT32, FIELD_SERVER_UID}, {"ReceivedDomain", SdReceivedDomain, GV_STRING, FIELD_RECEIVED_DOMAIN}, {"MessageId", SdMessageId, GV_STRING, FIELD_MESSAGE_ID}, {0} }; ////////////////////////////////////////////////////////////////////////////// class LIdentityItem : public LListItem { ScribeWnd *App; ScribeAccount *Acc; char *Txt; public: LIdentityItem(ScribeWnd *app, ScribeAccount *acc) { App = app; Acc = acc; Txt = 0; LVariant e, n; if (Acc) { n = Acc->Identity.Name(); e = Acc->Identity.Email(); } else { LAssert(!"No account specified"); } if (e.Str() && n.Str()) { char t[256]; sprintf_s(t, sizeof(t), "%s <%s>", n.Str(), e.Str()); Txt = NewStr(t); } else if (e.Str()) { Txt = NewStr(e.Str()); } else if (n.Str()) { Txt = NewStr(n.Str()); } else { Txt = NewStr("(error)"); } } ~LIdentityItem() { DeleteArray(Txt); } ScribeAccount *GetAccount() { return Acc; } const char *GetText(int i) { switch (i) { case 0: { return Txt; break; } } return 0; } }; class LIdentityDropDrop : public LPopup { ScribeWnd *App; Mail *Email; LList *Lst; public: LIdentityDropDrop(ScribeWnd *app, Mail *mail, LView *owner) : LPopup(owner) { App = app; Email = mail; LRect r(0, 0, 300, 100); SetPos(r); Children.Insert(Lst = new LList(IDC_LIST, 2, 2, X()-4, Y()-4)); if (Lst) { Lst->SetParent(this); Lst->AddColumn("Identity", Lst->GetClient().X()); Lst->MultiSelect(false); if (App) { Lst->Insert(new LIdentityItem(App, 0)); for (auto a : *App->GetAccounts()) { if (a->Identity.Name().Str()) { Lst->Insert(new LIdentityItem(App, a)); } } /* for (LListItem *i = List->First(); i; i = List->Next()) { LIdentityItem *Item = dynamic_cast(i); if (Item) { char *IdEmail = a->Send.IdentityEmail(); if (Email && IdEmail && Email->From->Addr && _stricmp(Email->From->Addr, IdEmail) == 0) { Item->Select(true); } } } */ } } } void OnPaint(LSurface *pDC) { LRect r(GetClient()); LWideBorder(pDC, r, DefaultRaisedEdge); } int OnNotify(LViewI *c, LNotification n) { switch (c->GetId()) { case IDC_LIST: { if (n.Type == LNotifyItemClick) { Visible(false); LIdentityItem *NewFrom = dynamic_cast(Lst->GetSelected()); if (Email && NewFrom) { // ScribeAccount *a = NewFrom->GetAccount(); if (Email->GetUI()) { LList *FromList; if (GetViewById(IDC_FROM, FromList)) { // Change item data /* FIXME DeleteArray(Email->From->Name); DeleteArray(Email->From->Addr); DeleteArray(Email->Reply->Name); DeleteArray(Email->Reply->Addr); if (a) { Email->From->Addr = NewStr(a->Send.IdentityEmail().Str()); Email->From->Name = NewStr(a->Send.IdentityName().Str()); Email->Reply->Addr = NewStr(a->Send.IdentityReplyTo().Str()); if (Email->Reply->Addr) { Email->Reply->Name = NewStr(Email->From->Name); } } else { LVariant e, n, r; App->GetOptions()->GetValue(OPT_EmailAddr, e); App->GetOptions()->GetValue(OPT_UserName, n); App->GetOptions()->GetValue(OPT_ReplyToEmail, n); Email->From->Addr = NewStr(e.Str()); Email->From->Name = NewStr(n.Str()); Email->Reply->Addr = NewStr(r.Str()); if (Email->Reply->Addr) { Email->Reply->Name = NewStr(Email->From->Name); } } // Change UI FromList->Empty(); LDataPropI *na = new LDataPropI(Email->From); if (na) { na->CC = MAIL_ADDR_FROM; FromList->Insert(na); } */ } } } } break; } } return 0; } }; ////////////////////////////////////////////////////////////////////////////// LSubMenu* BuildMarkMenu( LSubMenu *MarkMenu, MarkedState MarkState, uint32_t SelectedMark, bool None, bool All, bool Select) { int SelectedIndex = -1; // Build image list LImageList *ImgLst = new LImageList(16, 16); if (ImgLst && ImgLst->Create(16 * CountOf(MarkColours32), 16, System32BitColourSpace)) { // ImgLst->Colour(1); ImgLst->Colour(L_MED); ImgLst->Rectangle(); for (int i=0; iColour(L_LOW); ImgLst->Box(i*16+1, 0, i*16+15, 14); SelectedIndex = i; } ImgLst->Colour(MarkColours32[i], 32); ImgLst->Rectangle(i*16+3, 2, i*16+13, 12); } } // Build Submenu if (MarkMenu && ImgLst) { ImgLst->Update(-1); MarkMenu->SetImageList(ImgLst); LMenuItem *Item = NULL; if (None) { Item = MarkMenu->AppendItem(LLoadString(IDS_NONE), Select ? IDM_SELECT_NONE : IDM_UNMARK, MarkState != MS_None); } if (All) { Item = MarkMenu->AppendItem(LLoadString(IDS_ALL), Select ? IDM_SELECT_ALL : IDM_MARK_ALL, true); } if (Item) { MarkMenu->AppendSeparator(); } for (int i=0; iAppendItem(s, ((Select) ? IDM_MARK_SELECT_BASE : IDM_MARK_BASE) + i, (MarkState != 1) || (i != SelectedIndex)); if (Item) { Item->Icon(i); } } } return MarkMenu; } ////////////////////////////////////////////////////////////////////////////// char *WrapLines(char *Str, int Len, int WrapColumn) { if (Str && Len > 0 && WrapColumn > 0) { LMemQueue Temp; int LastWhite = -1; int StartLine = 0; int XPos = 0; int i; for (i=0; Str[i] && i= WrapColumn && Len > 0) { Temp.Write((uchar*) Str+StartLine, Len); Temp.Write((uchar*) "\n", 1); XPos = 0; StartLine = StartLine + Len + 1; LastWhite = -1; } else { LastWhite = i; XPos++; } } else if (Str[i] == '\t') { XPos = ((XPos + 7) / 8) * 8; } else if (Str[i] == '\n') { Temp.Write((uchar*) Str+StartLine, i - StartLine + 1); XPos = 0; StartLine = i+1; } else { XPos++; } } Temp.Write((uchar*) Str+StartLine, i - StartLine + 1); int WrapLen = (int)Temp.GetSize(); char *Wrapped = new char[WrapLen+1]; if (Wrapped) { Temp.Read((uchar*) Wrapped, WrapLen); Wrapped[WrapLen] = 0; return Wrapped; } } return Str; } char *DeHtml(const char *Str) { char *r = 0; if (Str) { LMemQueue Buf; char Buffer[256]; auto s = Str; while (s && *s) { // Search for start of next tag const char *Start = s; const char *End = Start; for (; *End && *End != '<'; End++); // Push pre-tag data onto pipe size_t Len = End-Start; for (size_t i=0; i, ItemFieldDef*> Lut(256); if (Lut.Length() == 0) { for (ItemFieldDef **l=FieldLists; *l; l++) { for (ItemFieldDef *i = *l; i->FieldId; i++) { if (i->Option) Lut.Add(i->Option, i); } } } return (ItemFieldDef*) Lut.Find(Name); } return 0; } static bool FieldLutInit = false; static LArray IdToFieldLut; ItemFieldDef *GetFieldDefById(int Id) { if (!FieldLutInit) { FieldLutInit = true; for (ItemFieldDef **l=FieldLists; *l; l++) { for (ItemFieldDef *i = *l; i->FieldId; i++) { IdToFieldLut[i->FieldId] = i; } } } if (Id >= 0 && Id < (int)IdToFieldLut.Length()) return IdToFieldLut[Id]; return 0; } ////////////////////////////////////////////////////////////////////////////// void Log(char *File, char *Str, ...) { #if defined WIN32 const char *DefFile = "c:\\temp\\list.txt"; #else const char *DefFile = "/home/list.txt"; #endif if (Str) { LFile f; if (f.Open((File) ? File : DefFile, O_WRITE)) { char Buf[1024]; va_list Arg; va_start(Arg, Str); vsprintf_s(Buf, sizeof(Buf), Str, Arg); va_end(Arg); f.Seek(0, SEEK_END); f.Write(Buf, (int)strlen(Buf)); } } else { LFile f; if (f.Open((File) ? File : DefFile, O_WRITE)) { f.SetSize(0); } } } char *NewPropStr(LOptionsFile *Options, char *Name) { LVariant n; if (Options->GetValue(Name, n) && n.Str() && strlen(n.Str()) > 0) { return NewStr(n.Str()); } return 0; } ////////////////////////////////////////////////////////////////////////////// // Columns of controls #define MAILUI_Y 0 #define IDM_REMOVE_GRTH 1000 #define IDM_REMOVE_GRTH_SP 1001 #define IDM_REMOVE_HTML 1002 #define IDM_CONVERT_B64_TO_BIN 1003 #define IDM_CONVERT_BIN_TO_B64 1004 #define RECIP_SX 500 #define ADD_X (RECIP_X + RECIP_SX + 10) #ifdef MAC #define ADD_RECIP_BTN_X 36 #else #define ADD_RECIP_BTN_X 20 #endif #define CONTENT_BORDER 3 #if defined WIN32 #define DLG_X 15 #define DLG_Y 30 #else #define DLG_X 6 #define DLG_Y 6 #endif MailUi::MailUi(Mail *item, MailContainer *container) : ThingUi(item, LLoadString(IDS_MAIL_MESSAGE)), WorkingDlg(NULL), Sx(0), Sy(0), CmdAfterResize(NULL), MissingCaps(NULL), BtnPrev(NULL), BtnNext(NULL), BtnSend(NULL), BtnSave(NULL), BtnSaveClose(NULL), BtnAttach(NULL), BtnReply(NULL), BtnReplyAll(NULL), BtnForward(NULL), BtnBounce(NULL), GpgUi(NULL), ToPanel(NULL), Entry(NULL), Browse(NULL), SetTo(NULL), To(NULL), Remove(NULL), FromPanel(NULL), FromList(NULL), FromCbo(NULL), ReplyToPanel(NULL), ReplyToChk(NULL), ReplyToCbo(NULL), SubjectPanel(NULL), Subject(NULL), CalendarPanel(NULL), CalPanelStatus(NULL), Tab(NULL), TabText(NULL), TextView(NULL), TabHtml(NULL), HtmlView(NULL), TabAttachments(NULL), Attachments(NULL), TabHeader(NULL), Header(NULL) { // Init everything to 0 Container = container; if (!item || !item->App) { LAssert(!"Invalid ptrs"); return; } AddMode = MAIL_ADDR_TO; CurrentEditCtrl = -1; MetaFieldsDirty = IgnoreShowImgNotify = HtmlCtrlDirty = TextCtrlDirty = TextLoaded = HtmlLoaded = false; // This allows us to hook iconv conversion events LFontSystem::Inst()->Register(this); // This allows us to hook missing image library events GdcD->Register(this); // Read/Write access bool ReadOnly = !TestFlag(GetItem()->GetFlags(), MAIL_CREATED | MAIL_BOUNCE); int MinButY = 0; // Get position LRect r(150, 150, 800, 750); SetPos(r); int FontHeight = GetFont()->GetHeight(); LVariant v; LOptionsFile *Options = App ? App->GetOptions() : 0; if (Options) { if (Options->GetValue("MailUI.Pos", v)) { r.SetStr(v.Str()); } } SetPos(r); MoveSameScreen(App); Name(LLoadString(IDS_MAIL_MESSAGE)); #if WINNATIVE CreateClassW32("Scribe::MailUi", LoadIcon(LProcessInst(), MAKEINTRESOURCE(IDI_MAIL))); #endif bool IsCreated = TestFlag(GetItem()->GetFlags(), MAIL_CREATED); if (Attach(0)) { DropTarget(true); // Setup main toolbar Commands.Toolbar = App->LoadToolbar(this, App->GetResourceFile(ResToolbarFile), App->GetToolbarImgList()); if (Commands.Toolbar) { Commands.Toolbar->Raised(false); Commands.Toolbar->Attach(this); BtnSend = Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_SEND)), IDM_SEND_MSG, TBT_PUSH, !ReadOnly, IMG_SEND); Commands.Toolbar->AppendSeparator(); BtnSave = Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_SAVE)), IDM_SAVE, TBT_PUSH, !ReadOnly, IMG_SAVE); BtnSaveClose = Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_SAVE_CLOSE)), IDM_SAVE_CLOSE, TBT_PUSH, !ReadOnly, IMG_SAVE_AND_CLOSE); Commands.Toolbar->AppendSeparator(); Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_DELETE)), IDM_DELETE_MSG, TBT_PUSH, true, IMG_TRASH); Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_SPAM)), IDM_DELETE_AS_SPAM, TBT_PUSH, true, IMG_DELETE_SPAM); BtnAttach = Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_ATTACH_FILE)), IDM_ATTACH_FILE, TBT_PUSH, !ReadOnly, IMG_ATTACH_FILE); Commands.Toolbar->AppendSeparator(); BtnReply = Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_REPLY)), IDM_REPLY, TBT_PUSH, ReadOnly, IMG_REPLY); BtnReplyAll = Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_REPLYALL)), IDM_REPLY_ALL, TBT_PUSH, ReadOnly, IMG_REPLY_ALL); BtnForward = Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_FORWARD)), IDM_FORWARD, TBT_PUSH, ReadOnly, IMG_FORWARD); BtnBounce = Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_BOUNCE)), IDM_BOUNCE, TBT_PUSH, ReadOnly, IMG_BOUNCE); Commands.Toolbar->AppendSeparator(); BtnPrev = Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_PREV_MSG)), IDM_PREV_MSG, TBT_PUSH, true, IMG_PREV_ITEM); BtnNext = Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_NEXT_MSG)), IDM_NEXT_MSG, TBT_PUSH, true, IMG_NEXT_ITEM); Commands.Toolbar->AppendSeparator(); Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_HIGH_PRIORITY)), IDM_HIGH_PRIORITY, TBT_TOGGLE, true, IMG_HIGH_PRIORITY); Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_LOW_PRIORITY)), IDM_LOW_PRIORITY, TBT_TOGGLE, true, IMG_LOW_PRIORITY); Commands.Toolbar->AppendSeparator(); Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_READ_RECEIPT)), IDM_READ_RECEIPT, TBT_TOGGLE, true, IMG_READ_RECEIPT); Commands.Toolbar->AppendSeparator(); Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_PRINT)), IDM_PRINT, TBT_PUSH, true, IMG_PRINT); for (LViewI *w: Commands.Toolbar->IterateViews()) MinButY = MAX(MinButY, w->Y()); Commands.Toolbar->Customizable(App->GetOptions(), "MailWindowToolbar"); Commands.SetupCallbacks(App, this, GetItem(), LThingUiToolbar); } // Setup to/from panels LVariant AlwaysShowFrom; if (IsCreated) App->GetOptions()->GetValue(OPT_MailShowFrom, AlwaysShowFrom); bool ShowFrom = !IsCreated && !TestFlag(GetItem()->GetFlags(), MAIL_BOUNCE); LDisplayString Recip(LSysFont, LLoadString(IDS_RECIPIENTS)); LAutoPtr ReplyChk(new LCheckBox(IDC_USE_REPLY_TO, 21, #ifdef MAC 1, #else 6, #endif -1, -1, "Reply To")); int ReplyChkPx = ReplyChk ? ReplyChk->X() : 0; int RECIP_X = 20 + MAX(ReplyChkPx, Recip.X()) + 10; int EditHeight = FontHeight + 6; LVariant HideGpg; if (!item->App->GetOptions()->GetValue(OPT_HideGnuPG, HideGpg) || HideGpg.CastInt32() == 0) { GpgUi = new MailUiGpg( App, this, 21, RECIP_X, !ReadOnly && !TestFlag(GetItem()->GetFlags(), MAIL_SENT)); if (GpgUi) GpgUi->Attach(this); } ToPanel = new LPanel(LLoadString(IDS_TO), FontHeight * 8, !ShowFrom); if (ToPanel) { int Cy = 4; ToPanel->AddView(SetTo = new LCombo(IDC_SET_TO, 20, Cy, 60, EditHeight, 0)); if (SetTo) { SetTo->Insert(LLoadString(IDS_TO)); SetTo->Insert(LLoadString(IDS_CC)); SetTo->Insert(LLoadString(IDS_BCC)); if (SetTo->GetPos().x2 + 10 > RECIP_X) RECIP_X = SetTo->GetPos().x2 + 10; } ToPanel->AddView(Entry = new LEdit(IDC_ENTRY, RECIP_X, Cy, RECIP_SX, EditHeight, "")); Cy = SetTo->GetPos().y2 + 5; ToPanel->AddView(To = new AddressList(App, IDC_TO, RECIP_X, Cy, RECIP_SX, ToPanel->GetOpenSize() - Cy - 6)); if (To) To->SetImageList(App->GetIconImgList(), false); ToPanel->AddView(new LTextLabel(IDC_STATIC, 20, Cy, -1, -1, LLoadString(IDS_RECIPIENTS))); ToPanel->Raised(false); ToPanel->Attach(this); } FromPanel = new LPanel(LLoadString(IDS_FROM), FontHeight + 13, AlwaysShowFrom.CastInt32() || ShowFrom); if (FromPanel) { FromPanel->AddView(new LTextLabel(IDC_STATIC, 21, #ifdef MAC 8, #else 4, #endif -1, -1, LLoadString(IDS_FROM))); if (ShowFrom) { FromPanel->AddView(FromList = new AddressList(App, IDC_FROM, RECIP_X, 2, RECIP_SX, EditHeight)); } else { FromPanel->AddView(FromCbo = new LCombo(IDC_FROM, RECIP_X, 2, RECIP_SX, EditHeight, 0)); } if (IsCreated) { LCheckBox *Chk; auto Label = LLoadString(IDS_ALWAYS_SHOW); FromPanel->AddView(Chk = new LCheckBox(IDC_SHOW_FROM, ADD_X, #ifdef MAC 1, #else 6, #endif -1, -1, Label)); if (Chk) Chk->Value(AlwaysShowFrom.CastInt32()); } FromPanel->Raised(false); FromPanel->Attach(this); } ReplyToPanel = new LPanel("Reply To:", FontHeight + 13, false); if (ReplyToPanel) { ReplyToPanel->Raised(false); ReplyToPanel->AddView(ReplyToChk = ReplyChk.Release()); ReplyToPanel->AddView(ReplyToCbo = new LCombo(IDC_REPLY_TO_ADDR, RECIP_X, 2, RECIP_SX, EditHeight, 0)); ReplyToPanel->Attach(this); } SubjectPanel = new LPanel(LLoadString(IDS_SUBJECT), -(LSysFont->GetHeight() + 16)); if (SubjectPanel) { SubjectPanel->Raised(false); SubjectPanel->AddView( new LTextLabel(IDC_STATIC, 21, 8, -1, -1, LLoadString(IDS_SUBJECT))); SubjectPanel->AddView(Subject = new LEdit(IDC_SUBJECT, RECIP_X, 4, RECIP_SX, EditHeight, "")); SubjectPanel->Attach(this); } CalendarPanel = new LPanel(LLoadString(IDC_CALENDAR), -(LSysFont->GetHeight() + 16), false); if (CalendarPanel) { CalendarPanel->Raised(false); LTableLayout *t = new LTableLayout(IDC_TABLE); if (t) { t->GetCss(true)->Margin(LCss::Len(LCss::LenPx, LTableLayout::CellSpacing)); t->GetCss()->Width(LCss::LenAuto); t->GetCss()->Height(LCss::LenAuto); CalendarPanel->AddView(t); auto c = t->GetCell(0, 0); c->Width(LCss::Len(LCss::LenPx, RECIP_X - (LTableLayout::CellSpacing * 2))); c->PaddingLeft(LCss::Len(LCss::LenPx, 21 - LTableLayout::CellSpacing)); c->Debug = true; c->Add(new LTextLabel(IDC_STATIC, 0, 0, -1, -1, LLoadString(IDS_CALENDAR))); c = t->GetCell(1, 0); c->Add(new LButton(IDC_ADD_CAL_EVENT, 0, 0, -1, -1, LLoadString(IDS_ADD_CAL))); c = t->GetCell(2, 0); c->Add(new LButton(IDC_ADD_CAL_EVENT_POPUP, 0, 0, -1, -1, LLoadString(IDS_ADD_CAL_POPUP))); c = t->GetCell(3, 0); c->Add(CalPanelStatus = new LTextLabel(IDC_STATIC, 0, 0, -1, -1, NULL)); } CalendarPanel->Attach(this); } Tab = new LTabView(IDC_MAIL_UI_TABS, 0, 0, 1000, 1000, 0); if (Tab) { Tab->GetCss(true)->PaddingTop("3px"); // Don't attach text and html controls here, because OnLoad will do it later... TabText = Tab->Append(LLoadString(IDS_TEXT)); TabHtml = Tab->Append("HTML"); TabAttachments = Tab->Append(LLoadString(IDS_ATTACHMENTS)); TabHeader = Tab->Append(LLoadString(IDS_INTERNETHEADER)); if (TabAttachments) { TabAttachments->Append(Attachments = new AttachmentList(IDC_ATTACHMENTS, CONTENT_BORDER, CONTENT_BORDER, 200, 200, this)); if (Attachments) { Attachments->AddColumn(LLoadString(IDS_FILE_NAME), 160); Attachments->AddColumn(LLoadString(IDS_SIZE), 80); Attachments->AddColumn(LLoadString(IDS_MIME_TYPE), 100); Attachments->AddColumn(LLoadString(IDS_CONTENT_ID), 250); } } if (TabHeader) { TabHeader->Append(Header = new LTextView3( IDC_INTERNET_HEADER, CONTENT_BORDER, CONTENT_BORDER, 100, 20)); if (Header) { Header->Sunken(true); } } LTabPage *Fields = Tab->Append(LLoadString(IDS_FIELDS)); if (Fields) { Fields->LoadFromResource(IDD_MAIL_FIELDS); LTableLayout *l; if (GetViewById(IDC_TABLE, l)) { l->SetPourLargest(false); } } Tab->Attach(this); // Colours LColourSelect *Colour; if (GetViewById(IDC_COLOUR, Colour)) { LArray c32; for (int i=0; iSetColourList(&c32); } else LAssert(!"No colour control?"); } PourAll(); if (Commands.Toolbar) { if (ToPanel) ToPanel->SetClosedSize(Commands.Toolbar->Y()-1); if (FromPanel) FromPanel->SetClosedSize(Commands.Toolbar->Y()-1); if (ReplyToPanel) ReplyToPanel->SetClosedSize(Commands.Toolbar->Y()-1); } SetIcon("About64px.png"); OnLoad(); _Running = true; Visible(true); RegisterHook(this, LKeyEvents); LResources::StyleElement(this); } } MailUi::~MailUi() { DeleteObj(TextView); if (Attachments) { Attachments->RemoveAll(); } LOptionsFile *Options = (GetItem() && App) ? App->GetOptions() : 0; if (Options) { LRect p = GetPos(); if (p.x1 >= 0 && p.y1 >= 0) { LVariant v = p.GetStr(); Options->SetValue("MailUI.Pos", v); } } Tab = 0; if (GetItem()) { GetItem()->Ui = NULL; } else LgiTrace("%s:%i - Error: no item to clear UI ptr?\n", _FL); // We delete the LView here because objects // need to have their virtual tables intact _Delete(); } Mail *MailUi::GetItem() { return _Item ? _Item->IsMail() : 0; } void MailUi::SetItem(Mail *m) { if (_Item) { Mail *old = _Item->IsMail(); if (old) old->Ui = NULL; else LAssert(0); _Item = NULL; } if (m) { _Item = m; m->Ui = this; } } bool MailUi::SetDirty(bool Dirty, bool Ui) { bool b = ThingUi::SetDirty(Dirty, Ui); if (MetaFieldsDirty && !Dirty) { Mail *m = GetItem(); if (m) { LColourSelect *Colour; if (GetViewById(IDC_COLOUR, Colour)) { uint32_t Col = (uint32_t)Colour->Value(); m->SetMarkColour(Col); } else LgiTrace("%s:%i - Can't find IDC_COLOUR\n", _FL); auto s = GetCtrlName(IDC_LABEL); m->SetLabel(s); if (m->GetObject()->GetInt(FIELD_STORE_TYPE) != Store3Imap) // Imap knows to save itself. m->SetDirty(); m->Update(); } MetaFieldsDirty = false; } return b; } #define IDM_FILTER_BASE 2000 #define IDM_CHARSET_BASE 3000 void AddActions(LSubMenu *Menu, List &Filters, LArray Folders) { if (!Menu) return; auto StartLen = Menu->Length(); for (auto Folder: Folders) { for (ScribeFolder *f=Folder->GetChildFolder(); f; f=f->GetNextFolder()) { auto Sub = Menu->AppendSub(f->GetName(true)); if (Sub) { auto Item = Sub->GetParent(); if (Item) Item->Icon(ICON_CLOSED_FOLDER); Sub->SetImageList(Menu->GetImageList(), false); f->LoadThings(); AddActions(Sub, Filters, {f}); } } List a; Folder->LoadThings(); for (auto t: Folder->Items) { a.Insert(t->IsFilter()); } a.Sort(FilterCompare); for (auto i: a) { LVariant Name; if (i->GetVariant("Name", Name) && Name.Str()) { char n[256]; strcpy_s(n, sizeof(n), Name.Str()); for (char *s=n; *s; s++) { if (*s == '&') { memmove(s + 1, s, strlen(s)+1); s++; } } auto Item = Menu->AppendItem(n, (int) (IDM_FILTER_BASE + Filters.Length()), true); if (Item) { Item->Icon(ICON_FILTER); Filters.Insert(i); } } } } if (StartLen == Menu->Length()) { char s[64]; sprintf_s(s, sizeof(s), "(%s)", LLoadString(IDS_EMPTY)); Menu->AppendItem(s, 0, false); } } bool MailUi::OnViewKey(LView *v, LKey &k) { if (k.Down() && k.CtrlCmd() && !k.Alt()) { switch (k.vkey) { case LK_RETURN: { PostEvent(M_COMMAND, IDM_SEND_MSG); return true; } case LK_UP: { SeekMsg(-1); return true; } case LK_DOWN: { SeekMsg(1); return true; } } switch (k.c16) { case 'p': case 'P': { App->ThingPrint(NULL, GetItem(), NULL, this); break; } case 'f': case 'F': { if (Tab->Value() == 0) { if (TextView) TextView->DoFind(NULL); } else if (Tab->Value() == 1) { if (HtmlView) HtmlView->DoFind(NULL); } return true; } 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 '1': { Tab->Value(0); if (TextView) TextView->Focus(true); return true; } case '2': { 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; bool DisabledLook = false; LString TextMem; 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 { auto rootSeg = GetObject()->GetObj(FIELD_MIME_SEG); if (rootSeg) MimeType = rootSeg->GetStr(FIELD_MIME_TYPE); if (!MimeType) return NULL; // This is the 'multipart/encrypted' case here... TextMem.Printf("'%s' content.", MimeType.Get()); TextBody = TextMem; DisabledLook = ReadOnly = true; } } #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) return NULL; // Control setup View->Sunken(Sunken); View->SetReadOnly(ReadOnly); View->SetEnv(this); if (DisabledLook) { View->GetCss(true)->BackgroundColor(L_MED); View->GetCss()->Color(L_LOW); } else { View->GetCss(true)->BackgroundColor(LCss::ColorInherit); View->GetCss()->Color(LCss::ColorInherit); } 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 { // Fall back... 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 = Atoi(Array); auto Txt = GetBody(); auto Html = GetHtml(); if (ValidStr(Txt) && ValidStr(Html)) { LVariant Def; App->GetOptions()->GetValue(OPT_DefaultAlternative, Def); if (Def.CastInt32()) { goto DoHtml; } goto DoText; } else if (ValidStr(Txt)) { DoText: LAutoString CharSet = GetCharSet(); if (CharSet) { size_t TxtLen = strlen(Txt); Value.OwnStr((char*)LNewConvertCp("utf-8", Txt, CharSet, MIN(TxtLen, MaxSize))); } else Value = Txt; } else if (ValidStr(Html)) { DoHtml: auto cs = GetHtmlCharset(); auto v = HtmlToText(Html, cs); Value = v.Get(); } else return false; break; } case SdBodyAsHtml: // Type: String { // int MaxSize = ValidStr(Array) ? atoi(Array) : -1; MailPrivate::HtmlBody *b = d->GetBody(); if (!b) return false; LStringPipe p; p.Print("
\n%s\n
\n", ScribeReplyClass, b->Html.Get()); Value.OwnStr(p.NewStr()); LgiTrace("Value=%s\n", Value.Str()); break; } case SdHtmlHeadFields: // Type: String { MailPrivate::HtmlBody *b = d->GetBody(); if (!b) return false; LStringPipe p; if (ValidStr(b->Charset)) p.Print("\t\n", b->Charset.Get()); p.Print("\t"); Value.OwnStr(p.NewStr()); break; } case SdMessageId: // Type: String { Value = GetMessageId(); return true; } case SdInternetHeaders: // Type: String { Value = GetInternetHeader(); break; } case SdInternetHeader: // Type: String[] { if (!Array || !GetInternetHeader()) return false; LAutoString s(InetGetHeaderField(GetInternetHeader(), Array)); if (s) Value = s; else Value.Empty(); break; } case SdPriority: // Type: Int32 { Value = (int)GetPriority(); break; } case SdHtml: // Type: String { Value = GetHtml(); break; } case SdFlags: // Type: Int32 { if (Array) { int Flag = StringToMailFlag(Array); Value = (GetFlags() & Flag) != 0; } else { Value = (int)GetFlags(); } break; } case SdFolder: // Type: ScribeFolder { ScribeFolder *f = GetFolder(); if (!f) return false; Value = (LDom*)f; break; } case SdScribe: // Type: ScribeWnd { Value = (LDom*)App; break; } case SdMail: // Type: Mail { Value = PreviousMail; break; } case SdDateSent: // Type: DateTime { Value = GetDateSent(); break; } case SdDateReceived: // Type: DateTime { Value = GetDateReceived(); break; } case SdSig: // Type: String { bool Type = false; if (Array && stristr(Array, "html")) Type = true; Value = GetSig(Type); break; } case SdSize: // Type: Int64 { Value = TotalSizeof(); break; } case SdLabel: // Type: String { Value = GetLabel(); break; } case SdAttachments: // Type: Int32 { LArray Lst; GetAttachmentObjs(Lst); Value = (int)Lst.Length(); break; } case SdAttachment: // Type: Attachment[] { List Files; if (GetAttachments(&Files) && Array) { int i = atoi(Array); Value = Files[i]; if (Value.Type == GV_DOM) return true; } LAssert(!"Not a valid attachment?"); return false; } case SdMimeTree: // Type: String { LDataI *Root = dynamic_cast(GetObject()->GetObj(FIELD_MIME_SEG)); if (!Root) return false; LStringPipe p(256); DescribeMime(p, Root); Value.OwnStr(p.NewStr()); break; } case SdUi: // Type: LView { Value = Ui; break; } case SdType: // Type: Int32 { Value = GetObject()->Type(); break; } case SdSelected: // Type: Bool { Value = Select(); break; } case SdRead: // Type: Bool { Value = (GetFlags() & MAIL_READ) != 0; break; } case SdShowImages: // Type: Bool { Value = (GetFlags() & MAIL_SHOW_IMAGES) != 0; break; } case SdColour: // Type: Int32 { Value = GetMarkColour(); break; } case SdReceivedDomain: // Type: String { Value = GetFieldText(FIELD_RECEIVED_DOMAIN); break; } default: { return false; } } return true; } #define DomSetStr(Dom, Fld) \ case Dom: \ if (GetObject() && Value.Type == GV_STRING) \ GetObject()->SetStr(Fld, Value.Str()); \ else \ LAssert(!"Missing object or type err"); \ break; #define DomSetInt(Dom, Fld) \ case Dom: \ if (GetObject() && Value.Type == GV_INT32) \ GetObject()->SetInt(Fld, Value.Value.Int); \ else \ LAssert(!"Missing object or type err"); \ break; #define DomSetDate(Dom, Fld) \ case Dom: \ if (GetObject() && Value.Type == GV_DATETIME) \ GetObject()->SetDate(Fld, Value.Value.Date); \ else \ LAssert(!"Missing object or type err"); \ break; bool Mail::SetVariant(const char *Name, LVariant &Value, const char *Array) { ScribeDomType Fld = StrToDom(Name); switch (Fld) { DomSetStr(SdSubject, FIELD_SUBJECT) DomSetStr(SdMessageId, FIELD_MESSAGE_ID) DomSetStr(SdInternetHeaders, FIELD_INTERNET_HEADER) DomSetInt(SdPriority, FIELD_PRIORITY) DomSetDate(SdDateSent, FIELD_DATE_SENT) DomSetDate(SdDateReceived, FIELD_DATE_RECEIVED) case SdBody: { if (!SetBody(Value.Str())) return false; break; } case SdHtml: { if (!SetHtml(Value.Str())) return false; break; } case SdRead: { if (Value.CastInt32()) SetFlags(GetFlags() | MAIL_READ); else SetFlags(GetFlags() & ~MAIL_READ); break; } case SdColour: { auto u32 = Value.IsNull() ? 0 : (uint32_t)Value.CastInt32(); if (!SetMarkColour(u32)) return false; break; } case SdShowImages: { if (Value.CastInt32()) SetFlags(GetFlags() | MAIL_SHOW_IMAGES); else SetFlags(GetFlags() & ~MAIL_SHOW_IMAGES); break; } case SdSelected: { Select(Value.CastInt32() != 0); break; } case SdLabel: { if (!SetLabel(Value.Str())) return false; break; } case SdFlags: { if (Array) { int Flag = StringToMailFlag(Array); if (Value.CastInt32()) // Set SetFlags(GetFlags() | Flag); else SetFlags(GetFlags() & ~Flag); } else { SetFlags(Value.CastInt32()); } break; } default: { return false; } } SetDirty(); Update(); return true; } bool Mail::CallMethod(const char *MethodName, LVariant *ReturnValue, LArray &Args) { ScribeDomType Fld = StrToDom(MethodName); switch (Fld) { default: break; case SdSend: // Type: ([Bool SendNow = true]) { bool Now = Args.Length() > 0 ? Args[0]->CastInt32() != 0 : true; bool Result = Send(Now); if (ReturnValue) *ReturnValue = Result; return true; } case SdAddCalendarEvent: // Type: ([Bool AddPopupReminder]) { bool AddPopupReminder = Args.Length() > 0 ? Args[0]->CastInt32() != 0 : true; bool Result = AddCalendarEvent(NULL, AddPopupReminder, NULL); if (ReturnValue) *ReturnValue = Result; return true; } case SdGetRead: // Type: () { *ReturnValue = (GetFlags() & MAIL_READ) != 0; return true; } case SdSetRead: // Type: (Bool IsRead = true) { bool Rd = Args.Length() ? Args[0]->CastInt32() != 0 : true; auto Flags = GetFlags(); if (Rd) SetFlags(Flags | MAIL_READ); else SetFlags(Flags & ~MAIL_READ); *ReturnValue = true; return true; } case SdBayesianChange: // Type: (int SpamWordOffset, int HamWordOffset) { if (Args.Length() != 2) { LgiTrace("%s:%i - Invalid arg count, expecting (int SpamWordOffset, int HamWordOffset)\n", _FL); *ReturnValue = false; return true; } auto SpamWordOffset = Args[0]->CastInt32(); auto HamWordOffset = Args[1]->CastInt32(); if (SpamWordOffset > 1) App->OnBayesianMailEvent(this, BayesMailUnknown, BayesMailSpam); else if (SpamWordOffset < 0) App->OnBayesianMailEvent(this, BayesMailSpam, BayesMailUnknown); if (HamWordOffset > 1) App->OnBayesianMailEvent(this, BayesMailUnknown, BayesMailHam); else if (HamWordOffset < 1) App->OnBayesianMailEvent(this, BayesMailHam, BayesMailUnknown); break; } case SdBayesianScore: // Type: () { double Result; if (App->IsSpam(Result, this)) { *ReturnValue = Result; return true; } break; } case SdSearchHtml: // Type: (String SearchExpression) { if (Args.Length() != 2) { LgiTrace("%s:%i - Method needs 1 argument.\n", _FL); *ReturnValue = false; return true; } auto Html = GetHtml(); if (!Html) { LgiTrace("%s:%i - No HTML to parse.\n", _FL); *ReturnValue = false; return true; } // auto Cs = GetHtmlCharset(); SearchHtml(ReturnValue, Html, Args[0]->Str(), Args[1]->Str()); return true; } case SdDeleteAsSpam: // Type: () { DeleteAsSpam(App); return true; } } return Thing::CallMethod(MethodName, ReturnValue, Args); } char *Mail::GetDropFileName() { if (!DropFileName) { LString Subj = GetSubject(); Subj = Subj.Strip(" \t\r\n."); DropFileName.Reset(MakeFileName(ValidStr(Subj) ? Subj.Get() : (char*)"Untitled", "eml")); } return DropFileName; } bool Mail::GetDropFiles(LString::Array &Files) { if (!GetDropFileName()) return false; if (!LFileExists(DropFileName)) { LAutoPtr F(new LFile); if (F->Open(DropFileName, O_WRITE)) { F->SetSize(0); if (!Export(AutoCast(F), sMimeMessage)) return false; } else return false; } if (!LFileExists(DropFileName)) return false; Files.Add(DropFileName.Get()); return true; } OsView Mail::Handle() { return #if LGI_VIEW_HANDLE (Ui) ? Ui->Handle() : #endif NULL; } Thing &Mail::operator =(Thing &t) { Mail *m = t.IsMail(); if (m) { #define CopyStr(dst, src) \ { DeleteArray(dst); dst = NewStr(src); } /* for (LDataPropI *a = m->GetTo()->First(); a; a = m->GetTo()->Next()) { LDataPropI *NewA = GetTo()->Create(Object->GetStore()); if (NewA) { *NewA = *a; GetTo()->Insert(NewA); } } */ if (GetObject() && m->GetObject()) { GetObject()->CopyProps(*m->GetObject()); } else LAssert(!"This shouldn't happen right?"); Update(); } return *this; } bool Mail::HasAlternateHtml(Attachment **Attach) { // New system of storing alternate HTML in a Mail object field. if (GetHtml()) return true; // Old way of storing alternate HTML, in an attachment. // pre v1.53 List Files; if (GetAttachments(&Files)) { for (auto a: Files) { if ((_stricmp(a->GetText(0), "attachment.html") == 0 || _stricmp(a->GetText(0), "index.html") == 0) && (a->GetMimeType() ? _stricmp(a->GetMimeType(), "text/html") == 0 : 1)) { if (Attach) { *Attach = a; } return true; } } } // None found return false; } char *Mail::GetAlternateHtml(List *Refs) { char *Status = 0; Attachment *Attach = 0; if (HasAlternateHtml(&Attach)) { if (GetHtml()) { // New system of storing alternate HTML in a Mail object field. Status = NewStr(GetHtml()); } else if (Attach) { // Old way of storing alternate HTML, in an attachment. // pre v1.53 char *Ptr; ssize_t Size; if (Attach->Get(&Ptr, &Size)) { Status = NewStr((char*)Ptr, Size); } } if (Status && Refs) { // Turn all the cid: references into file names... LStringPipe Out; char *n; for (char *s=Status; *s; s=n) { n = stristr(s, "\"cid:"); if (n) { // Extract cid n++; Out.Push(s, n-s); s = n += 4; while (*n && !strchr("\"\' >", *n)) n++; char *Cid = NewStr(s, n-s); if (Cid) { // Find attachment List Files; if (GetAttachments(&Files)) { for (auto a: Files) { if (a->GetContentId() && strcmp(a->GetContentId(), Cid) == 0) { Refs->Insert(a); Out.Push(a->GetName()); } } } } } else { Out.Push(s); break; } } DeleteArray(Status); Status = Out.NewStr(); } } return Status; } bool Mail::WriteAlternateHtml(char *DstFile, int DstFileLen) { bool Status = false; char *Tmp = ScribeTempPath(); List Refs; char *Html; if (Tmp && (Html = GetAlternateHtml(&Refs))) { char FileName[256]; LMakePath(FileName, sizeof(FileName), Tmp, "Alt.html"); if (DstFile) { strcpy_s(DstFile, DstFileLen, FileName); } LFile f; if (f.Open(FileName, O_WRITE)) { size_t Len = strlen(Html); Status = f.Write(Html, Len) == Len; f.Close(); } DeleteArray(Html); for (auto a: Refs) { LMakePath(FileName, sizeof(FileName), Tmp, a->GetName()); FileDev->Delete(FileName, false); a->SaveTo(FileName); } } return Status; } bool Mail::DeleteAttachment(Attachment *File) { bool Status = false; if (File) { if (Attachments.HasItem(File)) { Attachments.Delete(File); if (File->GetObject()) { auto r = File->GetObject()->Delete(); if (r > Store3Error) { auto o = File->GetObject(); if (o->IsOrphan()) o = NULL; // SetObject will delete... File->SetObject(NULL, false, _FL); DeleteObj(o); } } File->DecRef(); File = NULL; if (Attachments.Length() == 0) { // Remove attachments flag SetFlags(GetFlags() & (~MAIL_ATTACHMENTS)); } Update(); if (Ui) { Ui->OnAttachmentsChange(); } } } return Status; } bool Mail::UnloadAttachments() { for (auto it = Attachments.begin(); it != Attachments.end(); ) { Attachment *a = *it; if (!a->DecRef()) it++; } return true; } LArray Mail::GetAttachments() { LArray result; LArray Lst; if (GetAttachmentObjs(Lst)) { LHashTbl, bool> Loaded; for (size_t i=0; iGetObject(), true); } // Load attachments for (unsigned i=0; iSetOwner(this); Attachments.Insert(k); } } } } for (auto a: Attachments) { if (a->GetObject()->UserData != a) LAssert(!"Wut?"); else result.Add(a); } return result; } bool Mail::GetAttachments(List *Files) { if (!Files) return false; auto files = GetAttachments(); for (auto a: files) Files->Add(a); return true; } int64 Mail::TotalSizeof() { if (TotalSizeCache < 0) { int Size = GetObject() ? (int)GetObject()->GetInt(FIELD_SIZE) : -1; if (Size >= 0) { TotalSizeCache = Size; } else { TotalSizeCache = ((GetBody()) ? strlen(GetBody()) : 0) + ((GetHtml()) ? strlen(GetHtml()) : 0); List Attachments; if (GetAttachments(&Attachments)) { for (auto a: Attachments) { TotalSizeCache += a->GetSize(); } } } } return TotalSizeCache; } bool Mail::_GetListItems(List &l, bool All) { LList *ParentList = LListItem::Parent; l.Empty(); if (All) { if (ParentList) { ParentList->GetAll(l); } else { l.Insert(this); } } else { if (ParentList) { ParentList->GetSelection(l); } else if (Select()) { l.Insert(this); } } return l.Length() > 0; } void Mail::GetThread(List &Thread) { MContainer *c; for (c=Container; c->Parent; c=c->Parent); List Stack; Stack.Insert(c); while ((c=Stack[0])) { Stack.Delete(c); for (unsigned i=0; iChildren.Length(); i++) Stack.Insert(c->Children[i]); if (c->Message && !Thread.HasItem(c->Message)) { Thread.Insert(c->Message); } } } void Mail::SetListRead(bool Read) { List Sel; if (!_GetListItems(Sel, false)) return; LArray a; for (auto t: Sel) { auto m = dynamic_cast(t); if (!m) continue; bool isRead = (m->GetFlags() & MAIL_READ) != 0; if (Read ^ isRead) a.Add(m->GetObject()); } if (!a.Length()) return; auto Store = a[0]->GetStore(); if (!Store) { LAssert(0); return; } LVariant read = MAIL_READ; Store->Change(a, FIELD_FLAGS, read, Read ? OpPlusEquals : OpMinusEquals); } void SetFolderCallback(LInput *Dlg, LViewI *EditCtrl, void *Param) { ScribeWnd *App = (ScribeWnd*) Param; auto Str = EditCtrl->Name(); auto Select = new FolderDlg(Dlg, App, MAGIC_MAIL, 0, Str); Select->DoModal([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); Thread->AppendItem(LLoadString(IDS_THREAD_DELETE), IDM_DELETE_THREAD); Thread->AppendItem(LLoadString(IDS_THREAD_IGNORE), IDM_IGNORE_THREAD); 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); s.Sub->AppendItem(LLoadString(IDS_REPARSE), IDM_REPARSE); s.Sub->AppendItem(LLoadString(IDS_PROPERTIES), IDM_PROPERTIES); #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_REPARSE: { if (!_GetListItems(Sel, false)) break; for (auto s: Sel) { Mail *m = dynamic_cast(s); if (m) m->Reparse(); } 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()) { SetHtmlCharset("utf-8"); SetHtml(Sig); } else { SetBodyCharset("utf-8"); 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::Reparse() { #ifdef _DEBUG _debug = true; #endif // This temporarily removes the attachments that will be // deleted by the call to ParseHeaders after this... ClearCachedItems(); Thing::Reparse(); } 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()); Store3ToLMime(&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); 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... 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(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: { auto s = GetSubject(); if (Strchr(s, '\n')) { char *e = Buf + sizeof(Buf) - 1; char *o = Buf; for (auto i = s; o 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()); } auto EditMimeType = App->EditCtrlMimeType(); auto 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.Length() == 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/src/ScribeSendReceive.cpp b/src/ScribeSendReceive.cpp --- a/src/ScribeSendReceive.cpp +++ b/src/ScribeSendReceive.cpp @@ -1,2850 +1,2849 @@ /* ** FILE: ScribeAccount.cpp ** AUTHOR: Matthew Allen ** DATE: 19/10/99 ** DESCRIPTION: Scribe account ** ** Copyright (C) 2002, Matthew Allen ** fret@memecode.com */ // Includes #include #include #include #include #include "Scribe.h" #include "lgi/common/OpenSSLSocket.h" #include "ScribePrivate.h" -#include "lgi/common/NetTools.h" #include "resdefs.h" #include "lgi/common/LgiRes.h" #include "ScribeAccountPreview.h" #include "ScribeUtils.h" #include "lgi/common/TextConvert.h" #define MAIL_POSTED_TO_GUI 0x0001 #define MAIL_EXPLICIT 0x0002 #define SECONDS(i) ((i)*1000) #define TRANSFER_WAIT_TIMEOUT SECONDS(30) #define DEFAULT_SOCKET_TIMEOUT SECONDS(15) LColour SocketMsgTypeToColour(LSocketI::SocketMsgType flags) { LColour col; switch (flags) { case LSocketI::SocketMsgInfo: case LSocketI::SocketMsgNone: col.Set(MAIL_INFO_COLOUR, 24); break; case LSocketI::SocketMsgSend: col.Set(MAIL_SEND_COLOUR, 24); break; case LSocketI::SocketMsgReceive: col.Set(MAIL_RECEIVE_COLOUR, 24); break; case LSocketI::SocketMsgError: col.Set(MAIL_ERROR_COLOUR, 24); break; case LSocketI::SocketMsgWarning: col.Set(MAIL_WARNING_COLOUR, 24); break; default: LAssert(0); break; } return col; } int MakeOpenFlags(ScribeAccount *a, bool Send) { int OpenFlags = 0; Accountlet *l = Send ? (Accountlet*)&a->Send : (Accountlet*)&a->Receive; auto Nam = a->Identity.Name(); auto Em = a->Identity.Email(); // auto SslSmtp = a->Send.UseSSL(); ScribeSslMode SslMode = (ScribeSslMode)l->UseSSL(); switch (SslMode) { case SSL_STARTTLS: OpenFlags |= MAIL_USE_STARTTLS; break; case SSL_DIRECT: OpenFlags |= MAIL_SSL; break; default: break; } if (!Send && a->Receive.SecureAuth()) { OpenFlags |= MAIL_SECURE_AUTH; } if (Send) { if (a->Send.RequireAuthentication()) { OpenFlags |= MAIL_USE_AUTH; } switch (a->Send.AuthType()) { case 1: OpenFlags |= MAIL_USE_PLAIN; break; case 2: OpenFlags |= MAIL_USE_LOGIN; break; case 3: OpenFlags |= MAIL_USE_CRAM_MD5; break; case 4: OpenFlags |= MAIL_USE_OAUTH2; break; } } else { switch (a->Receive.AuthType()) { case 1: OpenFlags |= MAIL_USE_PLAIN; break; case 2: OpenFlags |= MAIL_USE_LOGIN; break; case 3: OpenFlags |= MAIL_USE_NTLM; break; case 4: OpenFlags |= MAIL_USE_OAUTH2; break; } } return OpenFlags; } /* class LProtocolLogger : public LStreamI { LViewI *Wnd; int Msg; List *Log; public: LProtocolLogger(LViewI *wnd, int msg, List *log) { Wnd = wnd; Msg = msg; Log = log; } ssize_t Read(void *Buffer, ssize_t Size, int Flags = 0) { return -1; } ssize_t Write(const void *b, ssize_t len, int f = 0) { LogEntry *l = new LogEntry(SocketMsgTypeToColour((GSocketI::SocketMsgType)f)); if (l) { l->Add((const char*)b, len); Wnd->PostEvent(Msg, (LMessage::Param)Log, (LMessage::Param)l); } return len; } }; */ void MakeTempPath(char *Path, int PathSize) { // Create unique temporary filename char r[32]; do { sprintf_s(r, sizeof(r), "%X.eml", LRand()); LMakePath(Path, PathSize, ScribeTempPath(), r); } while (LFileExists(Path)); } const char *AccountThreadStateName(AccountThreadState i) { switch (i) { case ThreadIdle: return "ThreadIdle"; case ThreadSetup: return "ThreadSetup"; case ThreadConnecting: return "ThreadConnecting"; case ThreadTransfer: return "ThreadTransfer"; case ThreadWaiting: return "ThreadWaiting"; case ThreadDeleting: return "ThreadDeleting"; case ThreadDone: return "ThreadDone"; case ThreadCancel: return "ThreadCancel"; case ThreadError: return "ThreadError"; } return "(none)"; } const char *ReceiveActionName(ReceiveAction i) { switch (i) { case MailNoop: return "MailNoop"; case MailDelete: return "MailDelete"; case MailDownloadAndDelete: return "MailDownloadAndDelete"; case MailDownload: return "MailDownload"; case MailUpload: return "MailUpload"; case MailHeaders: return "MailHeaders"; default: break; } return "(error)"; } const char *ReceiveStatusName(ReceiveStatus i) { switch (i) { case MailReceivedNone: return "ReceiveNone"; case MailReceivedWaiting: return "ReceiveWaiting"; case MailReceivedOk: return "ReceiveOk"; case MailReceivedError: return "ReceiveError"; default: break; } return "(none)"; } bool Accountlet::WaitForTransfers(List &Files) { LArray Counts; auto StartTs = LCurrentTime(); do { Counts.Length(0); for (auto e: Files) Counts[e->Status]++; LSleep(100); if (LCurrentTime() - StartTs > TRANSFER_WAIT_TIMEOUT) { LgiTrace("%s:%i - WaitForTransfers timed out.\n", _FL); for (ReceiveStatus rs = MailReceivedNone; rs < MailReceivedMax; rs = (ReceiveStatus)(((int)rs)+1)) LgiTrace("...%s=%i\n", ReceiveStatusName(rs), Counts[rs]); return false; } else if (Thread && Thread->IsCancelled()) { LgiTrace("%s:%i - WaitForTransfers exiting because thread is cancelled.\n", _FL); return false; } } while (Counts[MailReceivedWaiting] > 0); return true; } //////////////////////////////////////////////////////////////////////////// char *NewStrCat(char *a, char *b) { size_t Len = ((a)?strlen(a):0) + ((b)?strlen(b):0); char *s = new char[Len+1]; if (s) { s[0] = 0; if (a) strcat(s, a); if (b) strcat(s, b); } DeleteArray(a); return s; } class AccountletThread : public AccountThread { friend class SendAccountlet; friend class ReceiveAccountlet; friend bool AfterReceived(MailTransaction *Trans, void *Data); List Files; void MakeTempPath(char *Buf); void SetState(AccountThreadState s); AccountThreadState GetState(); public: // Object - Api AccountletThread(Accountlet *acc, List *Input); ~AccountletThread(); // Methods void Delete(int Index); void Complete(); // Called by the app when the all the files have been processed // void Cancel(); // Called by the app to signal the user is canceling the transfer // Thread int Main(); }; //////////////////////////////////////////////////////////////////////////// AccountletThread::AccountletThread(Accountlet *acc, List *Input) : AccountThread(acc) { } AccountletThread::~AccountletThread() { Files.DeleteObjects(); } void AccountletThread::SetState(AccountThreadState s) { Acc->State = s; } AccountThreadState AccountletThread::GetState() { return Acc->GetState(); } void AccountletThread::Delete(int Index) { MailTransferEvent *e = Files[Index]; if (e) { e->Action = MailDelete; } } void AccountletThread::Complete() { for (auto f: Files) { f->Rfc822Msg.Reset(); } Acc->State = ThreadDeleting; } int AccountletThread::Main() { // We're running... SetState(ThreadSetup); // Do all the connect, transfer and clean up Acc->Main(this); // This tells the app we're all done SetState(ThreadIdle); return 0; } //////////////////////////////////////////////////////////////////////////// LList *MailTransferEvent::GetList() { if (!Account) { LAssert(!"No account."); return NULL; } // As the list is set in the GUI thread, we should only ever use it in the // GUI thread... so check for that here. Otherwise we need to lock it while it's // in use. Which is not currently done. #ifdef _DEBUG ScribeWnd *App = Account->GetApp(); #endif LAssert(App->InThread()); ReceiveAccountlet *ra = dynamic_cast(Account); return ra ? ra->GetItems() : NULL; } //////////////////////////////////////////////////////////////////////////// Accountlet::Accountlet(ScribeAccount *a) : PrivLock("Accountlet") { DataStore = NULL; MailStore = NULL; Root = 0; Account = a; ConnectionStatus = true; OptPassword = 0; Client = 0; LastOnline = 0; Parent = 0; State = ThreadIdle; // Update sig to new format... char Buf[256]; LVariant Old; if (GetApp()->GetOptions()->GetValue(OptionName("AccSig", Buf, sizeof(Buf)), Old)) { if (LFileExists(Old.Str())) { char *Xml = LReadTextFile(Old.Str()); if (Xml) { GetAccount()->Identity.TextSig(Xml); DeleteArray(Xml); } } GetApp()->GetOptions()->DeleteValue(Buf); } } Accountlet::~Accountlet() { I Lck = Lock(_FL); if (Lck) { Lck->d->Log.DeleteObjects(); Lck.Reset(); } DeleteObj(Root); DeleteObj(DataStore); } void Accountlet::OnBeforeDelete() { // This has to be before ~ScribeAccount is finished, so that the // account's index is still valid, if we are going to save the "Expanded" // setting. LString p; if (DataStore && Root) { p = Root->GetPath(); bool e = Root->Expanded(); Expanded(e); } } bool Accountlet::GetVariant(const char *Name, LVariant &Value, const char *Array) { char k[128]; bool mapped = Account->IsMapped(OptionName(Name, k, sizeof(k))); auto Opts = GetApp()->GetOptions(); Value.Empty(); if (mapped) Opts->GetValue(k, Value); return true; } bool Accountlet::SetVariant(const char *Name, LVariant &Value, const char *Array) { char k[128]; bool mapped = Account->IsMapped(OptionName(Name, k, sizeof(k))); auto Opts = GetApp()->GetOptions(); if (mapped) Opts->SetValue(k, Value); return true; } void Accountlet::StrOption(const char *Opt, LVariant &v, const char *Set) { char Key[128]; OptionName(Opt, Key, sizeof(Key)); if (Set) GetApp()->GetOptions()->SetValue(Key, v = Set); else GetApp()->GetOptions()->GetValue(Key, v); } void Accountlet::IntOption(const char *Opt, LVariant &v, int Set) { char Key[128]; OptionName(Opt, Key, sizeof(Key)); if (Set >= 0) GetApp()->GetOptions()->SetValue(Key, v = Set); else GetApp()->GetOptions()->GetValue(Key, v); } const char *Accountlet::GetStateName() { switch (State) { case ThreadIdle: return LLoadString(IDS_ACCSTAT_IDLE); case ThreadSetup: return LLoadString(IDS_ACCSTAT_SETUP); case ThreadConnecting: return LLoadString(IDS_ACCSTAT_CONNECT); case ThreadTransfer: return LLoadString(IDS_ACCSTAT_TRANS); case ThreadWaiting: return LLoadString(IDS_ACCSTAT_WAIT); case ThreadDeleting: return LLoadString(IDS_ACCSTAT_DELETING); case ThreadDone: return LLoadString(IDS_ACCSTAT_FINISHED); case ThreadCancel: return LLoadString(IDS_ACCSTAT_CANCELLED); case ThreadError: return LLoadString(IDS_ERROR); } return "(none)"; } void Accountlet::OnThreadDone() { Thread.Reset(); if (DataStore && Root) Expanded(Root->Expanded()); DeleteObj(Root); } ScribeWnd *Accountlet::GetApp() { LAssert(Account->Parent != NULL); return Account->Parent; } LSocketI *Accountlet::CreateSocket(bool Sending, LCapabilityClient *Caps, bool RawLFCheck) { LSocketI *Socket = 0; LVariant File; LVariant Socks5Proxy; LVariant UseSocks; LVariant Format = (int)NET_LOG_NONE; GetApp()->GetOptions()->GetValue(OPT_LogFile, File); GetApp()->GetOptions()->GetValue(OPT_LogFormat, Format); int SslMode = UseSSL(); if (SslMode > 0) { Socket = new SslSocket(this, Caps, SslMode == SSL_DIRECT, RawLFCheck); } else if ( GetApp()->GetOptions()->GetValue(OPT_UseSocks, UseSocks) && UseSocks.CastInt32() && GetApp()->GetOptions()->GetValue(OPT_Socks5Server, Socks5Proxy) && ValidStr(Socks5Proxy.Str())) { LVariant Socks5UserName; LVariant Socks5Password; GetApp()->GetOptions()->GetValue(OPT_Socks5UserName, Socks5UserName); GetApp()->GetOptions()->GetValue(OPT_Socks5Password, Socks5Password); LUri Server(Socks5Proxy.Str()); Socket = new ILogSocks5Connection( Server.sHost, Server.Port?Server.Port:1080, Socks5UserName.Str(), Socks5Password.Str(), this); } else { Socket = new ILogConnection(this); } if (Socket) { Socket->SetTimeout(DEFAULT_SOCKET_TIMEOUT); if (File.Str()) { Socket->SetValue(OPT_LogFile, File); Socket->SetValue(OPT_LogFormat, Format); } Socket->SetCancel(Thread); } return Socket; } void Accountlet::Disconnect() { if (Thread) { Thread->Disconnect(); } else { if (MailStore) Account->Parent->OnMailStore(&MailStore, false); if (Root) { if (DataStore && Root) Expanded(Root->Expanded()); Root->Remove(); DeleteObj(Root); } DeleteObj(DataStore); } } void Accountlet::Kill() { if (Thread) { Thread->Kill(); } } bool Accountlet::Lock() { return Account->Parent->Lock(_FL); } void Accountlet::Unlock() { Account->Parent->Unlock(); } ssize_t Accountlet::Write(const void *buf, ssize_t size, int flags) { if (!Account->GetApp()) return 0; LColour col = SocketMsgTypeToColour((LSocketI::SocketMsgType)flags); I Lck = Lock(_FL); if (Lck) { bool Match = false; LArray &Log = Lck->d->Log; if (Log.Length() > 0) Match = Log.Last()->GetColour() == col; if (Match) { Log.Last()->Add((const char*)buf, size); } else { LogEntry *le = new LogEntry(col); if (le) { le->Add((const char*)buf, size); Log.Add(le); } } } return size; } void Accountlet::OnOnlineChange(bool Online) { if (DataStore && Root && Expanded()) { // uint64 s = LCurrentTime(); Root->Expanded(true); // uint64 e = LCurrentTime(); // LgiTrace("Expanded took: %i\n", (int)(e-s)); } } bool Accountlet::Connect(LView *p, bool quiet) { bool Status = false; I Lck = Lock(_FL); if (Lck) { Lck->d->Log.DeleteObjects(); Lck.Reset(); } if (Lock()) { Parent = p; Quiet = quiet; if (!Thread) { ReceiveAccountlet *Receive = NULL; if (IsReceive() && (Receive = &GetAccount()->Receive) && Receive->IsPersistant()) { LVariant Proto = GetAccount()->Receive.Protocol(); ScribeWnd *Wnd = GetAccount()->Parent; ScribeProtocol Protocol = ProtocolStrToEnum(Proto.Str()); #ifdef ImapSupport LString HostName = Server().Str(); if (!DataStore && Protocol == ProtocolImapFull && ValidStr(HostName)) { char Password[256] = ""; LPassword p; if (GetPassword(&p)) { p.Get(Password); } int OpenFlags = MakeOpenFlags(Account, false); MailProtocolProgress *Prog[2] = { &Group, &Item }; LAutoPtr Store; char StorePath[128]; if (OptionName(NULL, StorePath, sizeof(StorePath))) { Store.Reset(new ProtocolSettingStore(Wnd->GetOptions(), StorePath)); } DataStore = OpenImap(HostName, Receive->Port(), UserName().Str(), Password, OpenFlags, Account->GetApp(), GetAccount(), Prog, this, Id(), Store); if (DataStore) { DeleteObj(Root); if ((Root = new ScribeFolder)) { Root->App = Account->GetApp(); Root->SetLoadOnDemand(); LDataFolderI *r = DataStore->GetRoot(); if (r) Root->SetObject(r, false, _FL); Wnd->Tree->Insert(Root); } } } #endif #ifdef WIN32 if (!DataStore && Protocol == ProtocolMapi && ValidStr(Server().Str())) { char Password[256] = ""; LPassword p; if (GetPassword(&p)) p.Get(Password); DataStore = OpenMapiStore( Server().Str(), UserName().Str(), Password, Account->Receive.Id(), Account->GetApp()); if (DataStore) { LDataFolderI *r = DataStore->GetRoot(); if (r) { DeleteObj(Root); if ((Root = new ScribeFolder)) { Root->App = Account->GetApp(); Root->SetLoadOnDemand(); Root->SetObject(r, false,_FL); Wnd->Tree->Insert(Root); } } else LgiTrace("%s:%i - Failed to get data store root.\n", _FL); } } #endif } else { // Setup Enabled(false); I Lck = Lock(_FL); if (Lck) { Lck->d->Log.DeleteObjects(); Lck.Reset(); } auto StartThread = [this]() { if (Thread.Reset(new AccountletThread(this, 0))) { Thread->Run(); Account->Parent->OnBeforeConnect(Account, IsReceive()); } }; // Do we need the password? LString Password; LPassword Psw; GetPassword(&Psw); Password = Psw.Get(); auto User = UserName(); if (User.Str() && !ValidStr(Password)) { auto RemoteHost = Server(); LString Msg; Msg.Printf(LLoadString(IDS_ASK_ACCOUNT_PASSWORD), User.Str(), RemoteHost.Str()); GetApp()->GetUserInput( Parent ? Parent : GetApp(), Msg, true, [this, StartThread](auto Psw) { this->TempPsw = Psw; StartThread(); }); } else StartThread(); } } Unlock(); } return Status; } void Accountlet::IsCancelled(bool b) { if (Thread) Thread->Cancel(b); } bool Accountlet::IsCancelled() { return Thread ? Thread->IsCancelled() : false; } char *Accountlet::OptionName(const char *Opt, char *Dest, int DestLen) { int Idx = Account->GetIndex(); LAssert(Idx >= 0); if (Opt) { if (sprintf_s(Dest, DestLen, "Accounts.Account-%i.%s", Idx, Opt) < 0) return NULL; } else { if (sprintf_s(Dest, DestLen, "Accounts.Account-%i", Idx) < 0) return NULL; } return Dest; } bool Accountlet::GetPassword(LPassword *p) { bool Status = false; if (p && OptPassword && Lock()) { char Name[128]; OptionName(OptPassword, Name, sizeof(Name)); Status = p->Serialize(Account->GetApp()->GetOptions(), Name, false); Unlock(); } return Status; } void Accountlet::SetPassword(LPassword *p) { if (OptPassword && Lock()) { char Name[128]; OptionName(OptPassword, Name, sizeof(Name)); if (p) { p->Serialize(Account->GetApp()->GetOptions(), Name, true); } else { Account->GetApp()->GetOptions()->DeleteValue(Name); } Unlock(); } } bool Accountlet::IsCheckDialup() { LVariant CheckDialUp; char Name[128]; Account->GetApp()->GetOptions()->GetValue(OptionName(OPT_CheckForDialUp, Name, sizeof(Name)), CheckDialUp); return CheckDialUp.CastInt32() != 0; } //////////////////////////////////////////////////////////////////////////// AccountIdentity::AccountIdentity(ScribeAccount *a) : Accountlet(a) { } bool AccountIdentity::IsValid() { return ValidStr(Email().Str()) && ValidStr(Name().Str()); } void AccountIdentity::CreateMaps() { char Name[128]; Account->Map(OptionName(OPT_AccIdentName, Name, sizeof(Name)), IDC_NAME, GV_STRING); Account->Map(OptionName(OPT_AccIdentEmail, Name, sizeof(Name)), IDC_EMAIL, GV_STRING); Account->Map(OptionName(OPT_AccIdentReply, Name, sizeof(Name)), IDC_REPLY_TO, GV_STRING); Account->Map(OptionName(OPT_AccIdentTextSig, Name, sizeof(Name)), IDC_SIG, GV_STRING); Account->Map(OptionName(OPT_AccIdentHtmlSig, Name, sizeof(Name)), IDC_SIG_HTML, GV_STRING); } bool AccountIdentity::GetVariant(const char *Name, LVariant &Value, const char *Array) { /* case SdName: // Type: String case SdEmail: // Type: String case SdReply: // Type: String case SdSig: // Type: String case SdHtmlSig: // Type: String */ char n[128]; sprintf_s(n, sizeof(n), "Identity.%s", Name); return Accountlet::GetVariant(n, Value, Array); } bool AccountIdentity::SetVariant(const char *Name, LVariant &Value, const char *Array) { char n[128]; sprintf_s(n, sizeof(n), "Identity.%s", Name); return Accountlet::SetVariant(n, Value, Array); } //////////////////////////////////////////////////////////////////////////// SendAccountlet::SendAccountlet(ScribeAccount *a) : Accountlet(a) { SendItem = 0; // Options OptPassword = OPT_EncryptedSmtpPassword; } SendAccountlet::~SendAccountlet() { } bool SendAccountlet::GetVariant(const char *Name, LVariant &Value, const char *Array) { /* case SdServer: // Type: String case SdPort: // Type: Integer case SdDomain: // Type: String case SdName: // Type: String case SdAuth: // Type: Bool case SdAuthType: // Type: Integer case SdPrefCharset1: // Type: String case SdPrefCharset2: // Type: String case SdOnlySendThis: // Type: Bool case SdSSL: // Type: Integer */ char n[128]; sprintf_s(n, sizeof(n), "Send.%s", Name); return Accountlet::GetVariant(n, Value, Array); } bool SendAccountlet::SetVariant(const char *Name, LVariant &Value, const char *Array) { char n[128]; sprintf_s(n, sizeof(n), "Send.%s", Name); return Accountlet::SetVariant(n, Value, Array); } void SendAccountlet::CreateMaps() { // Fields char Name[256] = ""; Account->Map(OptionName(OPT_SmtpServer, Name, sizeof(Name)), IDC_SMTP_SERVER, GV_STRING); Account->Map(OptionName(OPT_SmtpPort, Name, sizeof(Name)), IDC_SMTP_PORT, GV_INT32); Account->Map(OptionName(OPT_SmtpDomain, Name, sizeof(Name)), IDC_SMTP_DOMAIN, GV_STRING); Account->Map(OptionName(OPT_SmtpName, Name, sizeof(Name)), IDC_SMTP_NAME, GV_STRING); Account->Map(OptionName(OPT_SmtpAuth, Name, sizeof(Name)), IDC_SMTP_AUTH, GV_BOOL); Account->Map(OptionName(OPT_SmtpAuthType, Name, sizeof(Name)), IDC_SEND_AUTH_TYPE, GV_INT32); Account->Map(OptionName(OPT_SendCharset1, Name, sizeof(Name)), IDC_SEND_CHARSET1, GV_STRING); Account->Map(OptionName(OPT_SendCharset2, Name, sizeof(Name)), IDC_SEND_CHARSET2, GV_STRING); Account->Map(OptionName(OPT_OnlySendThroughThis, Name, sizeof(Name)), IDC_ONLY_SEND_THIS, GV_BOOL); Account->Map(OptionName(OPT_SmtpSSL, Name, sizeof(Name)), IDC_SEND_SSL, GV_INT32); } ScribeAccountletStatusIcon SendAccountlet::GetStatusIcon() { return IsOnline() ? STATUS_ONLINE : STATUS_OFFLINE; } bool SendAccountlet::InitMenus() { // Menus LVariant n = Name(); LVariant s = Server(); LVariant u = UserName(); if (s.Str()) { char Name[256]; // Base name of menu if (n.Str()) { strcpy_s(Name, sizeof(Name), n.Str()); } else if (u.Str()) { sprintf_s(Name, sizeof(Name), "%s@%s", u.Str(), s.Str()); } else { strcpy_s(Name, sizeof(Name), s.Str()); } // Add send menuitem if (GetApp()->SendMenu) { LVariant Default; GetApp()->GetOptions()->GetValue(OPT_DefaultSendAccount, Default); if (Default.CastInt32() == Account->GetIndex()) { strcat(Name+strlen(Name), "\tCtrl-S"); } SendItem = GetApp()->SendMenu->AppendItem(Name, IDM_SEND_FROM+Account->GetIndex(), true); } return true; } return false; } void SendAccountlet::Enabled(bool b) { if (Account->GetIndex() == 0) { GetApp()->CmdSend.Enabled(b); } else if (SendItem) { SendItem->Enabled(b); } } void SendAccountlet::Main(AccountletThread *Thread) { bool Status = false; bool MissingConfig = false; LStringPipe Err; if (GetApp()) { LVariant v; GetApp()->GetOptions()->GetValue(OPT_DebugTrace, v); int DebugTrace = v.CastInt32(); LastOnline = LCurrentTime(); { MailSink *Sink; // LProtocolLogger Logger(GetApp(), M_SCRIBE_LOG_MSG, &Log); LVariant SendHotFolder = HotFolder(); if (SendHotFolder.Str() && LDirExists(SendHotFolder.Str())) { Client = Sink = new MailSendFolder(SendHotFolder.Str()); } else { Client = Sink = new MailSmtp; } if (Client) { LVariant s; if (GetApp()->GetOptions()->GetValue(OPT_ExtraHeaders, s)) { Client->ExtraOutgoingHeaders = s.Str(); } s = PrefCharset1(); if (s.Str()) { Sink->CharsetPrefs.Insert(NewStr(s.Str())); } s = PrefCharset2(); if (s.Str()) { Sink->CharsetPrefs.Insert(NewStr(s.Str())); } } if (Outbox.Length()) { int n = 0; for (unsigned Idx=0; IdxAction = MailUpload; t->Index = n++; t->Send = m; Thread->Files.Insert(t); } } if (DebugTrace) LgiTrace("Send(%i) got %i mail to send\n", Account->GetIndex(), Thread->Files.Length()); if (Sink && Thread->Files.Length()) { Sink->Logger = this; Sink->Transfer = &Item; if (Client) { LVariant _HotFld = HotFolder(); LVariant _Server = Server(); LVariant _Domain = Domain(); LVariant _UserName = UserName(); LString _Password; int Sent = 0; // int Total = Outbox.Length(); LVariant HideId = false; auto Opts = GetApp()->GetOptions(); Opts->GetValue(OPT_HideId, HideId); if (!HideId.CastInt32()) { Opts->GetValue(OPT_HideId, HideId); Client->ProgramName = GetFullAppName(!HideId.CastInt32()); } Client->SetSettingStore(Opts); if (ValidStr(_HotFld.Str()) || ValidStr(_Server.Str())) { bool Error = false; if (RequireAuthentication()) { LPassword p; if (GetPassword(&p)) _Password = p.Get(); } if (DebugTrace) LgiTrace("Send(%i) connecting to SMTP server\n", Account->GetIndex()); Thread->SetState(ThreadConnecting); int OpenFlags = MakeOpenFlags(Account, true); auto Params = GetOAuth2Params(_Server.Str(), MAGIC_MAIL); if (Params.IsValid()) { Sink->SetOAuthParams(Params); } if (!Sink->Open(CreateSocket(true, GetAccount(), true), _Server.Str(), _Domain.Str(), _UserName.Str(), _Password, Port(), OpenFlags)) { if (DebugTrace) LgiTrace("Send(%i) %s:%i\n", Account->GetIndex(), _FL); Err.Push(LLoadString(IDS_NO_CONNECTION_TO_SERVER)); Err.Push("\n"); } else { if (DebugTrace) LgiTrace("Send(%i) connected\n", Account->GetIndex()); Group.Value = 0; Group.Range = Thread->Files.Length(); Thread->SetState(ThreadTransfer); MailTransferEvent *e = NULL; for (auto FileIt = Thread->Files.begin(); FileIt != Thread->Files.end() && (e = dynamic_cast(*FileIt)) && !Thread->IsCancelled(); FileIt++, Group.Value++) { if (DebugTrace) LgiTrace("Send(%i) Item(%i) starting send\n", Account->GetIndex(), Group.Value); AddressDescriptor From; From.sAddr = e->Send->From; List ToLst; for (unsigned n=0; nSend->To.Length(); n++) { AddressDescriptor *ad = new AddressDescriptor; ad->sAddr = e->Send->To[n]; ToLst.Insert(ad); } MailProtocolError SendErr; LStringPipe *Out = Sink->SendStart(ToLst, &From, &SendErr); List *To = &ToLst; bool BadAddress = false; for (auto a: *To) { if (!a->Status) { if (a->sAddr.Find(",") >= 0) { auto t = a->sAddr.SplitDelimit(","); for (unsigned i=0; iPrint().Get()); } BadAddress = true; } } if (BadAddress) { LString m; m.Printf(LLoadString(Sink->ErrMsgId, Sink->ErrMsgFmt), Sink->ErrMsgParam.Get()); Err.Push(m.Get()); } if (Out) { LStringPipe InetHeaders; ssize_t Len = e->Send->Rfc822.Length(); ssize_t Written = 0; Item.Range = Len; Item.Start = LCurrentTime(); for (ssize_t Pos = 0; Pos < Len; ) { ssize_t Remaining = Len - Pos; ssize_t Block = MIN(Remaining, 4 << 10); ssize_t Wr = Out->Write(e->Send->Rfc822.Get() + Pos, Block); if (Wr <= 0) break; Pos += Wr; Written += Wr; Item.Value = Pos; } Item.Start = 0; if (Written == Len) { if (Sink->SendEnd(Out)) { if (DebugTrace) LgiTrace("Send(%i) Item(%i) success\n", Account->GetIndex(), Group.Value); Status = true; Sent++; e->OutgoingHeaders = InetHeaders.NewStr(); if (e->Send->References) { GetApp()->PostEvent(M_SCRIBE_SET_MSG_FLAG, (LMessage::Param)NewStr(e->Send->References), MAIL_REPLIED); } if (e->Send->FwdMsgId) { GetApp()->PostEvent(M_SCRIBE_SET_MSG_FLAG, (LMessage::Param)NewStr(e->Send->FwdMsgId), MAIL_FORWARDED); } if (e->Send->BounceMsgId) { GetApp()->PostEvent(M_SCRIBE_SET_MSG_FLAG, (LMessage::Param)NewStr(e->Send->BounceMsgId), MAIL_BOUNCED); } e->Account = this; e->Status = MailReceivedWaiting; GetApp()->OnMailTransferEvent(e); } else { Write("SendEnd failed.\n", -1, LSocketI::SocketMsgError); Error = true; break; } } else { Write("Encoding failed.\n", -1, LSocketI::SocketMsgError); Error = true; break; } } else { LString s; s.Printf("SendStart failed: %i, %s\n", SendErr.Code, SendErr.ErrMsg.Get()); Write(s, -1, LSocketI::SocketMsgError); Error = true; break; } } if (DebugTrace) LgiTrace("Send(%i) closing, Error=%i\n", Account->GetIndex(), Error); if (Error) { // we can't call Client->Close() on error because it // sends a quit command and expects a reply. Under // error conditions that can fail because the server // might not be in a state to handle that request. // So just close the socket and call it a day. if (DebugTrace) LgiTrace("Send(%i) %s:%i\n", Account->GetIndex(), _FL); Sink->CloseSocket(); } else { // Normal quit if (DebugTrace) LgiTrace("Send(%i) %s:%i\n", Account->GetIndex(), _FL); Sink->Close(); } if (DebugTrace) LgiTrace("Send(%i) %s:%i\n", Account->GetIndex(), _FL); Group.Value = 0; Group.Range = 0; Item.Value = 0; Item.Range = 0; Status &= WaitForTransfers(Thread->Files); } if (DebugTrace) LgiTrace("Send(%i) %s:%i, status=%i\n", Account->GetIndex(), _FL, Status); if (!Status) { if (DebugTrace) LgiTrace("Send(%i) %s:%i, Client=%p\n", Account->GetIndex(), _FL, Client); if (ValidStr(Client->ErrMsgFmt)) { LString m; m.Printf(LLoadString(Sink->ErrMsgId, Sink->ErrMsgFmt), Sink->ErrMsgParam.Get()); Err.Push("\n"); Err.Push(m.Get()); Err.Push("\n"); } } } else { MissingConfig = true; Err.Push(LLoadString(IDS_NO_SMTP_SERVER_SETTINGS)); Err.Push("\n"); } } if (DebugTrace) LgiTrace("Send(%i) %s:%i, Thread=%p\n", Account->GetIndex(), _FL, Thread); if (DebugTrace) LgiTrace("Send(%i) %s:%i\n", Account->GetIndex(), _FL); } Outbox.DeleteObjects(); } DeleteObj(Sink); } if (DebugTrace) LgiTrace("Send(%i) exit\n", Account->GetIndex()); } else { Thread->SetState(ThreadError); } char *e = 0; if (MissingConfig) { Err.Push(LLoadString(IDS_ASK_ABOUT_OPTIONS)); Err.Push("\n"); e = Err.NewStr(); GetApp()->PostEvent(M_SCRIBE_MSG, (LMessage::Param)e, 1); } else if ((e = Err.NewStr())) { GetApp()->PostEvent(M_SCRIBE_MSG, (LMessage::Param)e); } } //////////////////////////////////////////////////////////////////////////// ReceiveAccountlet::ReceiveAccountlet(ScribeAccount *a) : Accountlet(a) { // Init SecondsTillOnline = -1; LVariant Ct = CheckTimeout(); if (Ct.Str()) { SecondsTillOnline = GetCheckTimeout(); } ReceiveItem = 0; PreviewItem = 0; Items = 0; IdTemp = 0; char Key[128]; OptionName("Messages", Key, sizeof(Key)); Msgs.Reset(new MsgList(a->GetApp()->GetOptions(), Key)); OptionName("Spam", Key, sizeof(Key)); Spam.Reset(new MsgList(a->GetApp()->GetOptions(), Key)); // Upgrade props LVariant OldType = -1; char Buf[128]; LOptionsFile *Options = GetApp()->GetOptions(); if (Options->GetValue(OptionName(OPT_Pop3Type, Buf, sizeof(Buf)), OldType)) { // Old Account types: #define MAIL_SOURCE_POP3 0 #define MAIL_SOURCE_IMAP4 1 #define MAIL_SOURCE_MAPI 2 #define MAIL_SOURCE_SCP 3 #define MAIL_SOURCE_HTTP 4 switch (OldType.CastInt32()) { case MAIL_SOURCE_POP3: Protocol(PROTOCOL_POP3); break; case MAIL_SOURCE_IMAP4: Protocol(PROTOCOL_IMAP4_FETCH); break; case MAIL_SOURCE_SCP: Protocol(PROTOCOL_CALENDAR); break; case MAIL_SOURCE_HTTP: Protocol(PROTOCOL_POP_OVER_HTTP); break; case MAIL_SOURCE_MAPI: Protocol(PROTOCOL_MAPI); break; } Options->DeleteValue(Buf); } char Name[256] = ""; LVariant Auto; if (!Options->GetValue(OptionName(OPT_Pop3AutoReceive, Name, sizeof(Name)), Auto)) { LVariant s; Options->GetValue(OptionName(OPT_Pop3CheckEvery, Name, sizeof(Name)), s); if (s.Str()) { int Timeout = atoi(s.Str()); Options->SetValue(OptionName(OPT_Pop3AutoReceive, Name, sizeof(Name)), s = (Timeout > 0)); } } // Options OptPassword = OPT_EncryptedPop3Password; } ReceiveAccountlet::~ReceiveAccountlet() { } bool ReceiveAccountlet::GetVariant(const char *Name, LVariant &Value, const char *Array) { char n[128]; sprintf_s(n, sizeof(n), "Receive.%s", Name); return Accountlet::GetVariant(n, Value, Array); } bool ReceiveAccountlet::SetVariant(const char *Name, LVariant &Value, const char *Array) { char n[128]; sprintf_s(n, sizeof(n), "Receive.%s", Name); return Accountlet::SetVariant(n, Value, Array); } ScribeAccountletStatusIcon ReceiveAccountlet::GetStatusIcon() { if (DataStore) { int64 s = DataStore->GetInt(FIELD_STORE_STATUS); if (s >= STATUS_OFFLINE && s < STATUS_MAX) return (ScribeAccountletStatusIcon)s; } if (IsOnline()) return STATUS_ONLINE; if (!GetStatus()) return STATUS_ERROR; if (AutoReceive()) return STATUS_WAIT; return STATUS_OFFLINE; } void ReceiveAccountlet::CreateMaps() { char Name[256] = ""; // Fields Account->Map(OptionName(OPT_Pop3Protocol, Name, sizeof(Name)), IDC_REC_TYPE, GV_STRING); Account->Map(OptionName(OPT_Pop3Server, Name, sizeof(Name)), IDC_REC_SERVER, GV_STRING); Account->Map(OptionName(OPT_Pop3Port, Name, sizeof(Name)), IDC_REC_PORT, GV_INT32); Account->Map(OptionName(OPT_Pop3Name, Name, sizeof(Name)), IDC_REC_NAME, GV_STRING); Account->Map(OptionName(OPT_Pop3AutoReceive, Name, sizeof(Name)), IDC_CHECK_EVERY, GV_BOOL); Account->Map(OptionName(OPT_Pop3CheckEvery, Name, sizeof(Name)), IDC_REC_CHECK, GV_STRING); Account->Map(OptionName(OPT_Pop3LeaveOnServer, Name, sizeof(Name)), IDC_POP3_LEAVE, GV_BOOL); Account->Map(OptionName(OPT_DeleteAfter, Name, sizeof(Name)), IDC_DELETE_AFTER, GV_BOOL); Account->Map(OptionName(OPT_DeleteDays, Name, sizeof(Name)), IDC_DELETE_DAYS, GV_INT32); Account->Map(OptionName(OPT_DeleteIfLarger, Name, sizeof(Name)), IDC_DELETE_LARGER, GV_BOOL); Account->Map(OptionName(OPT_DeleteIfLargerSize, Name, sizeof(Name)), IDC_DELETE_SIZE, GV_INT32); Account->Map(OptionName(OPT_Pop3Folder, Name, sizeof(Name)), IDC_FOLDER, GV_STRING); Account->Map(OptionName(OPT_MaxEmailSize, Name, sizeof(Name)), IDC_MAX_SIZE, GV_INT32); Account->Map(OptionName(OPT_Receive8BitCs, Name, sizeof(Name)), IDC_REC_8BIT_CS, GV_STRING); Account->Map(OptionName(OPT_ReceiveAsciiCs, Name, sizeof(Name)), IDC_REC_ASCII_CP, GV_STRING); Account->Map(OptionName(OPT_ReceiveAuthType, Name, sizeof(Name)), IDC_RECEIVE_AUTH_TYPE, GV_INT32); Account->Map(OptionName(OPT_Pop3SSL, Name, sizeof(Name)), IDC_RECEIVE_SSL, GV_INT32); Account->Map(OptionName(OPT_ReceiveSecAuth, Name, sizeof(Name)), IDC_SEC_AUTH, GV_INT32); } int ReceiveAccountlet::GetCheckTimeout() { int Sec = -1; LVariant Timeout = CheckTimeout(); if (Timeout.Str()) { auto t = Timeout.LStr().SplitDelimit(":"); if (t.Length() == 2) { Sec = (atoi(t[0]) * 60) + atoi(t[1]); } else { Sec = atoi(Timeout.Str()) * 60; } } return Sec; } bool ReceiveAccountlet::InitMenus() { // Menus LVariant n = Name(); LVariant s = Server(); LVariant u = UserName(); if (IsConfigured()) { char Name[256]; // Base name of menu if (n.Str()) strcpy_s(Name, sizeof(Name), n.Str()); else if (u.Str() && s.Str()) sprintf_s(Name, sizeof(Name), "%s@%s", u.Str(), s.Str()); else if (u.Str()) strcpy_s(Name, sizeof(Name), u.Str()); else if (s.Str()) strcpy_s(Name, sizeof(Name), s.Str()); else strcpy_s(Name, sizeof(Name), "(untitled)"); // Add preview menuitem if (GetApp()->PreviewMenu) { PreviewItem = GetApp()->PreviewMenu->AppendItem(Name, IDM_PREVIEW_FROM+Account->GetIndex(), !Disabled()); } // Add shortcut if (Account->GetIndex() < 9) { size_t Len = strlen(Name); sprintf_s(Name+Len, sizeof(Name)-Len, "\tCtrl+%i", Account->GetIndex()+1); } // Add receive menuitem if (GetApp()->ReceiveMenu) { ReceiveItem = GetApp()->ReceiveMenu->AppendItem(Name, IDM_RECEIVE_FROM+Account->GetIndex(), !Disabled()); } return true; } return false; } bool ReceiveAccountlet::RemoveFromSpamIds(const char *Id) { if (!Id || !Spam) return false; if (!Lock()) return false; Spam->Delete(Id); Spam->Dirty = true; Unlock(); return true; } void ReceiveAccountlet::DeleteAsSpam(const char *Id) { if (HasMsg(Id)) { RemoveMsg(Id); if (Lock()) { if (Spam) { if (!Spam->Find(Id)) { Spam->Add(Id); Spam->Dirty = true; } } Unlock(); } } } bool ReceiveAccountlet::IsSpamId(const char *Id, bool Delete) { bool Status = false; if (ValidStr(Id)) { if (Lock()) { if (Spam) { Status = Spam->Find(Id); } Unlock(); } } return Status; } bool ReceiveAccountlet::SetActions(LArray *a) { bool Status = false; if (Lock()) { if (!IsOnline()) { if (a) Actions = *a; else Actions.Length(0); Status = true; } else LAssert(!"Trying to set actions when online is forbidden."); Unlock(); } return Status; } bool ReceiveAccountlet::SetItems(LList *l) { bool Status = false; if (Lock()) { Items = l; Status = true; Unlock(); } return Status; } void ReceiveAccountlet::Enabled(bool b) { if (Account->GetIndex() == 0) { GetApp()->CmdReceive.Enabled(b); GetApp()->CmdPreview.Enabled(b); } if (ReceiveItem) { ReceiveItem->Enabled(b); } if (PreviewItem) { PreviewItem->Enabled(b); } } int FilterCompare(Filter *a, Filter *b, NativeInt Data) { return a->GetIndex() - b->GetIndex(); } bool ReceiveAccountlet::IsPersistant() { LVariant Proto = Protocol(); if (Proto.Str()) { #ifdef ImapSupport if (_stricmp(Proto.Str(), PROTOCOL_IMAP4) == 0) return true; #endif #ifdef WIN32 if (!_stricmp(Proto.Str(), PROTOCOL_MAPI)) return true; #endif } return false; } int64 ConvertRelativeSize(char *Size, int64 Total) { int64 Status = -1; if (Size) { double f = atof(Size); if (strchr(Size, '%')) { Status = (int64)(Total * f) / 100; } else if (stristr(Size, "KB")) { Status = (int64)(f * 1024); } else if (stristr(Size, "MB")) { Status = (int64)(f * 1024 * 1024); } else if (stristr(Size, "GB")) { Status = (int64)(f * 1024 * 1024 * 1024); } else { Status = (int64)f; } } return Status; } struct MailReceiveParams { ScribeWnd *App; AccountletThread *Thread; uint64 MaxSize; uint64 NoAttachLimit; uint64 NoDownloadLimit; char *KitFileName; int DownloadLines; MailReceiveParams() { App = 0; Thread = 0; MaxSize = 0; NoAttachLimit = 0; NoDownloadLimit = 0; KitFileName = 0; DownloadLines = 0; } }; bool AfterReceived(MailTransaction *Trans, void *Data) { if (Trans->Oversize) { } else if (Trans->Status) { MailReceiveParams *p = (MailReceiveParams*) Data; if (!p) return false; MailTransferEvent *t = p->Thread->Files[Trans->Index]; if (!t) return false; Trans->Flags |= MAIL_POSTED_TO_GUI; if (t->Rfc822Msg) { // t->Data->Parse(); } // Set the event status t->Status = MailReceivedWaiting; t->Account = p->Thread->GetAccountlet(); // Ask the gui thread to load the mail in p->App->OnMailTransferEvent(t); } return true; } MailSrcStatus ReceiveCallback(MailTransaction *Trans, uint64 Size, int *TopLines, void *Data) { MailReceiveParams *Params = (MailReceiveParams*) Data; if (Params->KitFileName) { uint64 Free; if (LGetDriveInfo(Params->KitFileName, &Free)) { if (Free <= Params->NoDownloadLimit) { return DownloadNone; } else if (Free <= Params->NoAttachLimit) { if (TopLines && Params->DownloadLines > 0) { *TopLines = Params->DownloadLines; } return DownloadTop; } } } if (Params->MaxSize) { if (Size > Params->MaxSize) { if (!TestFlag(Trans->Flags, MAIL_EXPLICIT)) { return DownloadNone; } } } return DownloadAll; } bool ReceiveAccountlet::OnIdle() { return DataStore ? DataStore->OnIdle() : false; } void ReceiveAccountlet::Main(AccountletThread *Thread) { bool Status = false; LVariant v; LastOnline = LCurrentTime(); #define TimeDelta() ((int) (LCurrentTime() - LastOnline)) SecondsTillOnline = -1; int DebugTrace = false; GetApp()->GetOptions()->GetValue(OPT_DebugTrace, v); DebugTrace = v.CastInt32(); if (DebugTrace) LgiTrace("Receive(%i) starting, %i\n", Account->GetIndex(), TimeDelta()); auto RemoteHost = Server(); auto RemotePort = Port(); auto User = UserName(); // int LeaveCopy = LeaveOnServer(); int DeleteIfLargerThan = DeleteLarger() ? DeleteSize() << 10 : 0; MailReceiveParams Params; Params.App = GetApp(); Params.Thread = Thread; Params.MaxSize = DownloadLimit() << 10; auto MailSourceType = ProtocolStrToEnum(Protocol().Str()); // auto Ms = GetApp()->GetDefaultMailStore(); LString Password; LPassword Psw; GetPassword(&Psw); Password = Psw.Get(); MailSource *Source = 0; LVariant RecHotFolder = HotFolder(); if (RecHotFolder.Str() && LDirExists(RecHotFolder.Str())) { Client = Source = new MailReceiveFolder(RecHotFolder.Str()); } else { switch (MailSourceType) { case ProtocolPop3: { Client = Source = new MailPop3; break; } case ProtocolImapFetch: { Client = Source = new MailIMap; break; } case ProtocolPopOverHttp: { Client = Source = new MailPhp; break; } default: { LAssert(!"Unsupported protocol."); return; } } } if (DebugTrace) LgiTrace("Receive(%i) protocol=%i client=%p, time=%i\n", Account->GetIndex(), MailSourceType, Client, TimeDelta()); if (Source) { auto HttpProxy = GetApp()->GetHttpProxy(); if (HttpProxy) { LUri Host(HttpProxy); if (Host.sHost) Source->SetProxy(Host.sHost, Host.Port?Host.Port:80); } // Setup logging Source->Logger = this; Source->Items = &Group; Source->Transfer = &Item; auto OpenFlags = MakeOpenFlags(Account, false); auto NeedsPassword = !ValidStr(Password) && ValidStr(User.Str()); if (NeedsPassword) { if (TempPsw) Password = TempPsw; else if (!SecureAuth()) LAssert(!"Need to ask user for password BEFORE we're in the worker thread."); } if (DebugTrace) LgiTrace("Receive(%i) opening connection..., time=%i\n", Account->GetIndex(), TimeDelta()); Thread->SetState(ThreadConnecting); LHashTbl,bool> Uids; ReceiveAccountlet *Receive = dynamic_cast(Thread->Acc); if (!RecHotFolder.Str() && !SecureAuth() && !ValidStr(Password)) { Status = true; } else if (!Source->Open( CreateSocket(false, GetAccount(), false), RemoteHost.Str(), RemotePort, User.Str(), Password, SettingStore, OpenFlags)) { Thread->SetState(ThreadError); } else { if (DebugTrace) LgiTrace("Receive(%i) connected, time=%i\n", Account->GetIndex(), TimeDelta()); SecondsTillOnline = -1; // Get all the messages.. Thread->SetState(ThreadTransfer); auto Msgs = Source->GetMessages(); if (Msgs) { bool LeaveOnServer = Receive->LeaveOnServer() != 0; bool DeleteAfter = Receive->DeleteAfter() != 0; int DeleteDays = Receive->DeleteDays(); bool GetUids = LeaveOnServer; bool HasGetHeaders = false; if (DeleteDays < 1) { Receive->DeleteDays(DeleteDays = 1); } for (int i=0; iRfc822Msg.Reset(new LTempStream(ScribeTempPath())); t->Index = i; if (Actions.Length()) { t->Action = (unsigned)i < Actions.Length() ? Actions[i] : MailNoop; t->Explicit = true; } else if (Receive->Items) { t->Action = MailHeaders; GetUids = true; } else { t->Action = LeaveOnServer ? MailDownload : MailDownloadAndDelete; } switch (t->Action) { default: break; case MailHeaders: { HasGetHeaders = true; // fall thru } case MailDownloadAndDelete: case MailDownload: case MailUpload: { Group.Range++; break; } } Thread->Files.Insert(t); } } if (DebugTrace) LgiTrace("Receive(%i) got %i actions, time=%i\n", Account->GetIndex(), Thread->Files.Length(), TimeDelta()); if (HasGetHeaders || DeleteIfLargerThan) { LArray Sizes; if (Source->GetSizes(Sizes)) { unsigned i = 0; for (auto t: Thread->Files) { if (i >= Sizes.Length()) break; t->Size = Sizes[i]; if (t->Action == MailHeaders) { t->Msg = new AccountMessage(Account); if (t->Msg) { t->Msg->Size = t->Size; } } i++; } } } // Getting the UID's of the messages on the server if (GetUids || Receive->Msgs->Length() > 0) { if (DebugTrace) LgiTrace("Receive(%i) getting UID's, time=%i\n", Account->GetIndex(), TimeDelta()); LString::Array UidLst; Source->GetUidList(UidLst); for (auto u: UidLst) Uids.Add(u, true); // Assign all the ID strings to the transfer events LDateTime Now; Now.SetNow(); for (auto t: Thread->Files) { auto k = UidLst[0]; if (k) { UidLst.DeleteAt(0, true); t->Uid = k; if (DebugTrace) LgiTrace("\tUid[%i]='%s' (time=%i)\n", t->Index, t->Uid.Get(), TimeDelta()); } else break; if (!t->Explicit && DeleteAfter) { // Check how long the message has been on the server. LDateTime MsgDate; if (Receive->Msgs->GetDate(t->Uid, &MsgDate)) { LDateTime Days = Now - MsgDate; if (Days.Day() > DeleteDays) { if (t->Action == MailDownload) t->Action = MailDownloadAndDelete; } } } } } if (DebugTrace) LgiTrace("Receive(%i) starting main action loop, time=%i\n", Account->GetIndex(), TimeDelta()); Group.Start = LCurrentTime(); bool Error = false; LArray Trans; char NotLoaded[256]; sprintf_s( NotLoaded, sizeof(NotLoaded), "%s: %s", LLoadString(FIELD_SUBJECT), LLoadString(IDS_NOT_LOADED)); for (auto it = Thread->Files.rbegin(); it != Thread->Files.end() && !Thread->IsCancelled(); it--) { MailTransferEvent *t = *it; if (!t) continue; bool Ok = false; switch (t->Action) { case MailDownload: case MailDownloadAndDelete: { if (!t->Explicit && t->Uid && Receive->Msgs->Find(t->Uid)) { // Already got it... // if (DebugTrace) LgiTrace("Receive(%i) Item(%i) Already got msg\n", Account->GetIndex(), t->Index); Status = true; Group.Value++; if (DeleteIfLargerThan > 0 && t->Size > 0 && t->Size > DeleteIfLargerThan) { t->Action = MailDelete; } continue; } else { if (t->Uid && Receive->IsSpamId(t->Uid)) { // Delete the spam t->Action = MailDelete; Status = true; Group.Value++; continue; } else { // Download it MailTransaction *Get = new MailTransaction; if (Get) { if (t->Explicit) Get->Flags |= MAIL_EXPLICIT; Get->Index = t->Index; Get->Stream = t->Rfc822Msg; Trans.Add(Get); continue; } } Group.Value++; } break; } case MailHeaders: { if (DebugTrace) LgiTrace("Receive(%i) Item(%i) Getting headers..., time=%i\n", Account->GetIndex(), t->Index, TimeDelta()); auto Headers = Source->GetHeaders(t->Index); if (DebugTrace) LgiTrace("Receive(%i) Item(%i) headers=%p, time=%i\n", Account->GetIndex(), t->Index, Headers.Get(), TimeDelta()); if (!Headers) Headers = NotLoaded; if (Headers) { if (!t->Msg) t->Msg = new AccountMessage(Account); if (t->Msg) { bool Attachments = false; LAutoString ContentType(InetGetHeaderField(Headers, "Content-Type")); if (ContentType) { Attachments = stristr(ContentType, "multipart/mixed") != NULL; } t->Msg->Index = t->Index; LAutoString From(DecodeRfc2047(InetGetHeaderField(Headers, "From"))); LAutoString Subject(DecodeRfc2047(InetGetHeaderField(Headers, "Subject"))); t->Msg->From = From.Get(); t->Msg->Subject = LString(Subject).Replace("\n"); t->Msg->ServerUid = t->Uid; LAutoString d(InetGetHeaderField(Headers, "Date")); if (d) t->Msg->Date.Decode(d); t->Msg->Attachments = Attachments; if (IsSpamId(t->Uid)) { t->Msg->Download->Value(false); t->Msg->Delete->Value(true); t->Msg->New = false; } else { t->Msg->New = !HasMsg(t->Uid); } Ok = true; } } Group.Value++; break; } default: { continue; } } if (Ok) { Status = true; t->Account = this; t->Status = MailReceivedWaiting; GetApp()->OnMailTransferEvent(t); } else { if (DebugTrace) LgiTrace("Receive(%i) Item(%i) Error, time=%i\n", Account->GetIndex(), t->Index, TimeDelta()); Error = true; break; } } if (Trans.Length() > 0) { MailCallbacks Callbacks; ZeroObj(Callbacks); Callbacks.CallbackData = &Params; Callbacks.OnSrc = ReceiveCallback; Callbacks.OnReceive = AfterReceived; Error = !Source->Receive(Trans, &Callbacks); for (unsigned i=0; iFiles[Tran->Index]; if (t) { LgiTrace("%s:%i - Trans[%i]: No 't' ptr for idx=%i files.len=%i.\n", _FL, i, Tran->Index, (int)Thread->Files.Length()); } else { if (Tran->Oversize) { // Ignore t->Action = MailNoop; t->Status = MailReceivedOk; Status = true; } else if (Tran->Status) { if (t->Status == MailReceivedNone) { if (DebugTrace) LgiTrace("Receive(%i) Item(%i) Posting WM_SCRIBE_THREAD_ITEM, time=%i\n", Account->GetIndex(), t->Index, TimeDelta()); Status = true; if (!TestFlag(Tran->Flags, MAIL_POSTED_TO_GUI)) { // Ask the gui thread to load the mail in t->Status = MailReceivedWaiting; GetApp()->OnMailTransferEvent(t); } } else { Status = true; } } else { LgiTrace("%s:%i - Error: Bad Status on download %i of %i.\n", _FL, i, Trans.Length()); // Don't delete a mail that failed to download t->Action = MailNoop; } } } Trans.DeleteObjects(); } // Done... wait for main thread to finish processing if (DebugTrace) LgiTrace("Receive(%i) Waiting for main thread, time=%i\n", Account->GetIndex(), TimeDelta()); Thread->SetState(ThreadWaiting); // Wait for the main thread to finish with all the items we sent over if (!WaitForTransfers(Thread->Files)) Error = true; if (Error) { LgiTrace("%s:%i - Error receiving mail.\n", _FL); } else { // Do delete's if (DebugTrace) LgiTrace("Receive(%i) Delete phase, time=%i\n", Account->GetIndex(), TimeDelta()); Group.Empty(); { for (auto d: Thread->Files) { if (d->Action == MailDelete || d->Action == MailDownloadAndDelete) { Group.Range++; } } } Group.Start = LCurrentTime(); Thread->SetState(ThreadDeleting); for (auto It = Thread->Files.rbegin(); It != Thread->Files.end() && Thread->GetState() == ThreadDeleting; It--) { MailTransferEvent *d = *It; if (d->Action == MailDelete || d->Action == MailDownloadAndDelete) { if (DebugTrace) LgiTrace("Receive(%i) Delete(%i) Deleting, time=%i\n", Account->GetIndex(), d->Index, TimeDelta()); if (Source->Delete(d->Index)) { Status = true; if (d->Uid) { Uids.Delete(d->Uid); } } Group.Value++; } } } WaitForTransfers(Thread->Files); Thread->Files.DeleteObjects(); Group.Empty(); } else { Receive->RemoveAllMsgs(); Status = true; } // Clear out the UID's if (Uids.Length() && Receive->Msgs) { // ssize_t UidsLen = Uids.Length(); // ssize_t AllMsgIdsLen = Receive->Msgs->Length(); auto MsgKeys = Receive->Msgs->CopyKeys(); for (auto &k : MsgKeys) { if (!Uids.Find(k)) RemoveMsg(k); } if (Spam) { auto SpamKeys = Spam->CopyKeys(); for (auto &s : SpamKeys) { if (!Uids.Find(s)) Spam->Delete(s); } } } if (DebugTrace) LgiTrace("Receive(%i) Closing the connection, time=%i\n", Account->GetIndex(), TimeDelta()); // Close the connection. Source->Close(); } DeleteObj(Source); } else { Thread->SetState(ThreadError); } // Clean up, notify the app ConnectionStatus = Status; Actions.Length(0); if (DebugTrace) LgiTrace("Receive(%i) Exit, time=%i\n", Account->GetIndex(), TimeDelta()); } void ReceiveAccountlet::OnPulse(char *s, int s_len) { // this is called every second if (Lock()) { LVariant Offline; bool On = IsOnline(); LString Serv = Server().Str(); if (On) { strcat(s, "Online"); SecondsTillOnline = -1; if (Thread && Thread->GetState() == LThread::THREAD_EXITED) { #ifdef _DEBUG LgiTrace("Caught exited thread state, Accountlet[%i]=%p, Thread=%p, &Thread=%p\n", GetAccount()->GetIndex(), this, Thread.Get(), &Thread); #endif Thread.Reset(); } } else if ((GetApp()->GetOptions()->GetValue(OPT_WorkOffline, Offline) && Offline.CastInt32()) || Disabled() > 0) { strcat(s, "Offline"); SecondsTillOnline = -1; } else { if (AutoReceive()) { if (SecondsTillOnline < 0) { int To = GetCheckTimeout(); SecondsTillOnline = To > 0 ? To : 10 * 60; } } else { SecondsTillOnline = -1; } if (SecondsTillOnline == 0) { SecondsTillOnline = -1; if ((!IsCheckDialup() || HaveNetConnection()) && !Offline.CastInt32()) { Connect(0, false); } } else if (SecondsTillOnline > 0) { SecondsTillOnline--; if (CheckTimeout().Str()) { sprintf_s(s, s_len, "%i:%2.2i", SecondsTillOnline/60, SecondsTillOnline%60); } } } Unlock(); } } bool ReceiveAccountlet::HasMsg(const char *Id) { if (!Id || !Msgs || !Lock()) return false; auto Status = Msgs->Find(Id); Unlock(); return Status; } void ReceiveAccountlet::AddMsg(const char *Id) { if (!Msgs || !Lock()) return; if (!Msgs->Find(Id)) Msgs->Add(Id); Unlock(); } void ReceiveAccountlet::RemoveMsg(const char *Id) { if (!Id || !Msgs || !Lock()) return; Msgs->Delete(Id); Unlock(); } void ReceiveAccountlet::RemoveAllMsgs() { if (!Msgs || !Lock()) return; Msgs->Empty(); Unlock(); } int ReceiveAccountlet::GetMsgs() { int Status = 0; if (Lock()) { if (Msgs) { Status = Msgs->Length(); } Unlock(); } return Status; } ////////////////////////////////////////////////////////////////////////////////// #define OPT_MsgDate "Date" MsgList::MsgList(LOptionsFile *opts, char *tag) { Opts = opts; Tag = tag; Loaded = false; Dirty = false; } MsgList::~MsgList() { } bool MsgList::Load() { if (Opts && Tag && !Loaded) { LXmlTag *Msg = Opts->LockTag(Tag, _FL); if (!Msg) { Opts->CreateTag(Tag); Msg = Opts->LockTag(Tag, _FL); } if (Msg) { for (auto t: Msg->Children) { if (!t->GetAttr(OPT_MsgDate)) { LDateTime Now; char n[64]; Now.SetNow(); Now.Get(n, sizeof(n)); t->SetAttr(OPT_MsgDate, n); } Parent::Add(t->GetContent(), t); } Opts->Unlock(); } Loaded = true; } return Loaded; } LXmlTag *MsgList::LockId(const char *id, const char *file, int line) { if (Load()) { LXmlTag *r = Opts->LockTag(Tag, file, line); if (r) { LXmlTag *m = (LXmlTag*) Parent::Find(id); if (m) { // deliberately leave the options locked. return m; } Opts->Unlock(); } } return 0; } void MsgList::Unlock() { Opts->Unlock(); } bool MsgList::Add(const char *id) { bool Status = false; LXmlTag *r = Opts->LockTag(Tag, _FL); if (r) { if (!Parent::Find(id)) { LXmlTag *t = new LXmlTag("Message"); if (t) { LDateTime Now; char n[64]; Now.SetNow(); Now.Get(n, sizeof(n)); t->SetContent(id); t->SetAttr(OPT_MsgDate, n); r->InsertTag(t); Status = Parent::Add(id, t); } } Unlock(); } return Status; } bool MsgList::Delete(const char *id) { bool Status = false; LXmlTag *r = Opts->LockTag(Tag, _FL); if (r) { LXmlTag *t = Parent::Find(id); if (t) { // LgiTrace("DelMsg '%s' = %p\n", id, t); Parent::Delete(id); t->RemoveTag(); DeleteObj(t); Status = true; } Opts->Unlock(); } return Status; } int MsgList::Length() { if (Load()) { return (int)Parent::Length(); } return 0; } bool MsgList::Find(const char *id) { if (Load()) { return Parent::Find(id) != 0; } return 0; } LString::Array MsgList::CopyKeys() { LString::Array a(Length()); for (auto i : *this) { a.Add(i.key); } return a; } void MsgList::Empty() { if (!Load()) return; LXmlTag *Msg = Opts->LockTag(Tag, _FL); if (!Msg) return; LXmlTag *t; while ( Msg->Children.Length() && (t = Msg->Children[0])) { DeleteObj(t); } Opts->Unlock(); Parent::Empty(); } bool MsgList::SetDate(char *id, LDateTime *dt) { bool Status = false; if (id && dt) { LXmlTag *t = LockId(id, _FL); if (t) { char s[64]; LAssert(dt->IsValid()); // Force the date to save in a guessable format. // yyyy/m/d is always easy to sniff. dt->SetFormat(GDTF_YEAR_MONTH_DAY); dt->Get(s, sizeof(s)); t->SetAttr(OPT_MsgDate, s); Status = true; Unlock(); } } return Status; } bool MsgList::GetDate(char *id, LDateTime *dt) { if (!id || !dt) return false; LXmlTag *t = LockId(id, _FL); if (!t) return false; char *s = t->GetAttr(OPT_MsgDate); if (s) { // This is where I fucked up... previous versions of the software // saved the date in whatever the currently selected date format. // Which could've been anything... and the user can change that. // So if they do change it, the saved dates are now in the wrong // format to load back in (or mixed). Setting the format to '0' // here puts the LDateTime object into "guess" mode based on the // data. Which is going to be the closest we get to recovering // from the ugly situation of not saving the date in a specific // format. dt->SetFormat(0); dt->Set(s); LAssert(dt->IsValid()); } Unlock(); return s != NULL; } /////////////////////////////////////////////////////////////////////////////// LMimeStream::LMimeStream() : LTempStream(ScribeTempPath(), 4 << 20) { } #ifdef _DEBUG #define DEBUG_MIME_STREAM 1 #else #define DEBUG_MIME_STREAM 0 #endif bool LMimeStream::Parse() { if (Tmp) Tmp->SetPos(0); #if DEBUG_MIME_STREAM LMemStream *Src = dynamic_cast(s); #endif bool Status = Text.Decode.Pull(s) != 0; if (!Status) { #if DEBUG_MIME_STREAM LFile f; if (Src && f.Open("c:\\temp\\mime_error.txt", O_WRITE)) { Src->SetSize(0); Src->SetPos(0); Src->Write(&f, (int)Src->GetSize()); f.Close(); } s->SetPos(0); #endif LAssert(!"Mime Parsing Failed."); } LTempStream::Empty(); return Status; } diff --git a/src/Store3Imap/ScribeImap_Folder.cpp b/src/Store3Imap/ScribeImap_Folder.cpp --- a/src/Store3Imap/ScribeImap_Folder.cpp +++ b/src/Store3Imap/ScribeImap_Folder.cpp @@ -1,2584 +1,2584 @@ #include "lgi/common/Lgi.h" -#include "lgi/common/NetTools.h" + #include "ScribeImap.h" #include "ScribeUtils.h" #include "DomType.h" #if IMAP_PROTOBUF #include #endif #ifdef _DEBUG #define DEBUG_RENAME_OLD_XML 1 #else #define DEBUG_RENAME_OLD_XML 0 #endif #define TIMEOUT_IDLE_STATUS (60 * 1000) #define INVALID_CACHE -1000 ImapFolder::ImapFolder(ImapStore *store, const char *path) #if !IMAP_PROTOBUF : Meta(TAG_FOLDER) #endif { System = Store3SystemNone; Dirty = false; SortCache = INVALID_CACHE; IsOnline = false; LastIdleSelect = LCurrentTime(); _Parent = NULL; Store = store; ScanState = 0; Remote = path; #if IMAP_PROTOBUF ThreadView = false; PermRead = PermRequireNone; PermWrite = PermRequireNone; SortField = 0; Expanded = false; UnreadCount = 0; #endif ItemType = MAGIC_MAIL; if (Remote) { if (!_stricmp(Remote, "/")) ItemType = MAGIC_NONE; else if (!_stricmp(Remote, "/Trash") || !_stricmp(Remote, "/Deleted Items") || !_stricmp(Remote, "/Deleted Messages")) { ItemType = MAGIC_ANY; System = Store3SystemTrash; } char s[MAX_PATH_LEN] = ""; LMakePath(s, sizeof(s), Store->GetCache(), Remote); Local = s; char *Last = strrchr(Remote, '/'); if (Last++) { if (!_stricmp(Last, "Inbox")) System = Store3SystemInbox; else if (!_stricmp(Last, "Sent")) System = Store3SystemSent; else if (!_stricmp(Last, "Outbox")) System = Store3SystemOutbox; } Serialize(false); } } ImapFolder::~ImapFolder() { LoadThread.Reset(); Sub.DeleteObjects(); Field.DeleteObjects(); Mail.DeleteObjects(); // UidMap.DeleteObjects(); } void ImapFolder::SetParent(ImapFolder *p) { if (_Parent) { if (_Parent->Sub.State != Store3Loaded) LgiTrace("%s:%i - %p->Parent(%p) Sub not loaded :(\n", _FL, this, _Parent); else if (_Parent->Sub.IndexOf(this) < 0) LgiTrace("%s:%i - Folder(%p) not in parent(%p) Sub :(\n", _FL, this, _Parent); else _Parent->Sub.Delete(this); } _Parent = p; if (_Parent) { LAssert(_Parent->Sub.State != Store3Unloaded); _Parent->Sub.Insert(this, -1, true); } } void ImapFolder::SetDirty(bool b) { Dirty = b; if (Dirty && Mail.State != Store3Loaded) { LAssert(!"State needs to be loaded before we modify it."); } } struct ImapFolderWriterThread : public LThread { LString File; LString Data; ImapFolderWriterThread(const char *file, LString data) : LThread("WriterThread") { File = file; Data = data; Run(); } ~ImapFolderWriterThread() { uint64 Start = LCurrentTime(); while (!IsExited()) { LSleep(20); uint64 Now = LCurrentTime(); if (Now - Start > 1000) { LgiTrace("ImapFolder::WriterThread shutting down: %ims\n", (int)(Now-Start)); } } } int Main() { #if DEBUG_RENAME_OLD_XML if (LFileExists(File)) { // Don't overwrite... just rename auto parts = LString(File).RSplit(DIR_STR, 1); auto leaf = parts.Last().RSplit(".", 1); auto now = LDateTime::Now(); auto newleaf = leaf[0] + " " + now.Get().Replace(":","-").Replace("/","-") + "." + leaf[1]; auto dstFile = parts[0] + DIR_STR + newleaf; LError err; if (!FileDev->Move(File, dstFile, &err)) LgiTrace("%s:%i - Failed to rename '%s' -> '%s': %s\n", _FL, File.Get(), dstFile.Get(), err.GetMsg().Get()); } #endif LFile f; if (f.Open(File, O_WRITE)) { f.SetSize(0); ssize_t Wr = f.Write(Data, Data.Length()); f.Close(); if (Wr != Data.Length()) { LAssert(!"Failed to write all the XML"); LgiTrace("%s:%i - ImapFolder::WriterThread wrote %i of %i\n", _FL, (int)Wr, (int)Data.Length()); } } return 0; } }; bool ImapFolder::Serialize(bool Write) { bool Debug = false; // System == Store3SystemInbox; if (!ValidStr(Local)) return false; LFile::Path XmlPath(Local); XmlPath += FOLDER_META_NAME; LXmlTree t; LFile f; #if IMAP_PROTOBUF bool Status = false; if (Write) { LProfile p("ImapFolderWrite"); scribe::Folder folder; p.Add("Meta"); folder.set_readperm(PermRead); folder.set_writeperm(PermWrite); folder.set_sortfield(SortField); folder.set_itemtype(ItemType); folder.set_expanded(Expanded); folder.set_threadview(ThreadView); p.Add("Fields"); for (auto in: Field.a) { auto out = folder.add_fields(); out->set_id(in->GetInt(FIELD_ID)); out->set_width(in->GetInt(FIELD_WIDTH)); } p.Add("Email"); for (auto in: Mail.a) { try { #if 1 auto m = in->GetMeta(); if (m) { LAssert(m->IsInitialized()); folder.mutable_mail()->Add(std::move(*m)); } #else auto out = folder.add_mail(); if (in->Uid) out->set_uid(in->Uid); out->set_remoteflags(in->RemoteFlags.All); out->set_localflags(in->LocalFlags); if (in->DateSent.IsValid()) out->set_datesent(in->DateSent.Ts()); if (in->DateReceived.IsValid()) out->set_datereceived(in->DateReceived.Ts()); if (in->Label) out->set_label(in->Label); if (in->Subject) out->set_subject(in->Subject); out->set_size(in->DataSize); if (in->Path) out->set_filename(in->Path); out->set_priority(in->Priority); if (in->Structure) out->set_structure(in->Structure); if (in->From.Addr || in->From.Name) { auto a = new scribe::Address(); if (in->From.Addr) a->set_email(in->From.Addr.Get()); if (in->From.Name) a->set_name(in->From.Name.Get()); out->set_allocated_from(a); } for (auto to: in->To.a) { if (to->Addr || to->Name) { auto a = out->add_to(); if (to->Addr) a->set_email(to->Addr.Get()); if (to->Name) a->set_name(to->Name.Get()); } } if (in->Colour.IsValid()) out->set_colour(in->Colour.c32()); LAssert(out->IsInitialized()); #endif } catch (...) { LAssert(0); } } p.Add("FileWrite"); { LAssert(folder.IsInitialized()); std::ofstream file(s, std::ios_base::binary); if (Status = folder.SerializeToOstream(&file)) Dirty = false; } #if 0 { scribe::Folder f; std::ifstream file(s, std::ios_base::binary); bool r = f.ParseFromIstream(&file); LAssert(r); } #endif } else { Field.DeleteObjects(); Mail.DeleteObjects(); scribe::Folder folder; std::ifstream file(s, std::ios_base::binary); if (folder.ParseFromIstream(&file)) { // Meta PermRead = (ScribePerm)folder.readperm(); PermWrite = (ScribePerm)folder.writeperm(); SortField = folder.sortfield(); ItemType = folder.itemtype(); Expanded = folder.expanded(); ThreadView = folder.threadview(); // Fields: for (int i=0; iRemoteFlags.All = in.remoteflags(); out->LocalFlags = in.localflags(); out->DateSent.Set(in.datesent()); out->DateReceived.Set(in.datereceived()); out->Label = in.label().c_str(); out->Subject = in.subject().c_str(); out->DataSize = in.size(); out->Path = in.filename().c_str(); out->Priority = in.priority(); out->Structure = in.structure().c_str(); out->Parent = this; Mail.Insert(out, -1, true); // if (out->Uid) UidMap.Add(out->Uid, in); } } if (Mail.a.Length()) Mail.State = Store3Loaded; Status = true; } if (Field.Length() == 0) { // Set the default fields. Field.State = Store3Loaded; Field.Insert(new ImapFolderFld(GetStore(), this, FIELD_PRIORITY, 10)); Field.Insert(new ImapFolderFld(GetStore(), this, FIELD_FLAGS, 10)); Field.Insert(new ImapFolderFld(GetStore(), this, FIELD_FROM, 120)); Field.Insert(new ImapFolderFld(GetStore(), this, FIELD_SUBJECT, 200)); Field.Insert(new ImapFolderFld(GetStore(), this, FIELD_SIZE, 46)); Field.Insert(new ImapFolderFld(GetStore(), this, FIELD_DATE_SENT, 120)); #if 0 // def _DEBUG Field.Insert(new ImapFolderFld(GetStore(), this, FIELD_CACHE_FLAGS, 100)); Field.Insert(new ImapFolderFld(GetStore(), this, FIELD_CACHE_FILENAME, 140)); #endif Field.State = Store3Loaded; SetInt(FIELD_SORT, -6); SetDirty(); } } return Status; #else if (Write) { auto e = Meta.GetChildTag(TAG_FIELDS, true); uint64 t1; if (e) { e->EmptyChildren(); t1 = LCurrentTime(); for (unsigned i=0; iInsertTag(new LXmlTag(*f1)); } } else { LAssert(0); return false; } if (Mail.State != Store3Loaded) { LAssert(!"Not loaded? We can't overwrite the valid XML with invalid..."); return false; } if ((e = Meta.GetChildTag(TAG_EMAILS, true))) { for (auto it : UidMap) e->InsertTag(it.value); if (Debug) LgiTrace("%s:%i - Meta has " LPrintfSizeT " email.\n", _FL, e->Children.Length()); } LStringPipe Buf(16<<10); if (!t.Write(&Meta, &Buf)) { LAssert(0); return false; } if (Debug) LgiTrace("%s:%i - Buf has %s.\n", _FL, LFormatSize(Buf.GetSize()).Get()); auto Xml = Buf.NewLStr(); if (WriteThread.Reset(new ImapFolderWriterThread(XmlPath, Xml))) { if (e) e->Children.Empty(); Dirty = false; } else { LAssert(0); return false; } } else // read { Field.DeleteObjects(); if (LFileExists(XmlPath)) { LFile f; if (f.Open(XmlPath, O_READ) && t.Read(&Meta, &f, 0)) { if (Debug) LgiTrace("%s:%i - In file has %s.\n", _FL, LFormatSize(f.GetSize()).Get()); // Fields LXmlTag *t = Meta.GetChildTag(TAG_FIELDS); if (t && t->Children.Length() > 0) { for (auto c: t->Children) { int Id = c->GetAsInt("Id"); int Width = c->GetAsInt("Width"); ImapFolderFld *ff = new ImapFolderFld(GetStore(), this, Id, Width); if (ff) { Field.Insert(ff, -1, true); } } } Field.State = Store3Loaded; // Email if ((t = Meta.GetChildTag(TAG_EMAILS))) { // Move all the tags into the Sid hash table for (auto c: t->Children) { auto Uid = c->GetAsInt(ATTR_UID); if (!Uid) { LAssert(!"Not a valid email tag."); DeleteObj(c); } else { UidMap.Add(Uid, c); } } if (Debug) LgiTrace("%s:%i - Meta read " LPrintfSizeT " email.\n", _FL, UidMap.Length()); t->Children.Empty(); } } } if (Field.Length() == 0) { // Set the default fields. Field.State = Store3Loaded; Field.Insert(new ImapFolderFld(GetStore(), this, FIELD_PRIORITY, 10)); Field.Insert(new ImapFolderFld(GetStore(), this, FIELD_FLAGS, 10)); Field.Insert(new ImapFolderFld(GetStore(), this, FIELD_FROM, 120)); Field.Insert(new ImapFolderFld(GetStore(), this, FIELD_SUBJECT, 200)); Field.Insert(new ImapFolderFld(GetStore(), this, FIELD_SIZE, 46)); Field.Insert(new ImapFolderFld(GetStore(), this, FIELD_DATE_SENT, 120)); #if 0 // def _DEBUG Field.Insert(new ImapFolderFld(GetStore(), this, FIELD_CACHE_FLAGS, 100)); Field.Insert(new ImapFolderFld(GetStore(), this, FIELD_CACHE_FILENAME, 140)); #endif SetInt(FIELD_SORT, -6); SetDirty(); } } return true; #endif } Store3Status ImapFolder::Move(LArray &Items) { Store3Status Status = Store3Error; LHashTbl, bool> Has; LArray Moved; LAutoPtr Mv(new ImapMsg(IMAP_MOVE_EMAIL, _FL)); for (unsigned n=0; nType() == MAGIC_MAIL) { ImapMail *m = CastMail(Items[n]); if (m) { if (Has.Find(m->Uid)) { LAssert(!"Duplicate UID."); continue; } Has.Add(m->Uid, true); if (m->Parent) { if (!m->RemoteFlags.ImapDeleted) { if (!Mv->Parent) Mv->Parent = m->Parent->Remote.Get(); if (_stricmp(Mv->Parent, m->Parent->Remote) || Mv->Mail.Length() >= IMAP_BLOCK_SIZE) { Store->PostThread(Mv.Release(), false); Mv.Reset(new ImapMsg(IMAP_MOVE_EMAIL, _FL)); Mv->Parent = m->Parent->Remote.Get(); } ImapMailInfo &Inf = Mv->Mail.New(); Inf.Uid = m->Uid; if (!Mv->NewRemote) Mv->NewRemote = Remote.Get(); m->RemoteFlags.ImapDeleted = true; // It'll be set soon anyway... m->SetState(ImapMail::ImapMailMoving); Moved.Add(m); Status = Store3Delayed; } else LgiTrace("%s:%i - Can't move deleted email '%i'\n", _FL, m->Uid); } else LAssert(!"No parent."); } else LAssert(!"Not an imap mail."); } else if (Items[n]->Type() == MAGIC_FOLDER) { /* ImapFolder *f = dynamic_cast(Items[n]); if (f) { if (f->Parent) { char p[MAX_PATH_LEN], *Leaf = strrchr(Local, DIR_CHAR); if (Leaf) { Leaf++; LMakePath(p, sizeof(p), Local, Leaf); if (FileDev->Move(f->Remote, p)) { // Adjust path f->Remote.Reset(NewStr(p)); // Adjust containers and pointers LAssert(f->Parent->Sub.IndexOf(f) >= 0); f->Parent->Sub.Delete(f); f->Parent = this; LAssert(Sub.IndexOf(f) < 0); Sub.Insert(f); Status = true; // Adjust all the path's of the email in the ImapFolder for (int i=0; iMail.Length(); i++) { ImapMail *m = dynamic_cast(f->Mail[i]); if (m) { char *MailLeaf = strchr(m->Path, DIR_CHAR); if (MailLeaf) { char mpath[MAX_PATH_LEN]; LMakePath(mpath, sizeof(mpath), f->Remote, MailLeaf+1); m->Path.Reset(NewStr(mpath)); } else LAssert(!"Not a valid path."); } } Status = true; } } else LAssert(!"Not a valid path."); } else LAssert(!"No parent."); } else LAssert(!"Not a valid imap folder."); */ } else LAssert(!"Not a valid object type."); } if (Mv->Parent && Mv->Mail.Length()) { Store->PostThread(Mv.Release(), false); Status = Store3Delayed; } /* This should be handled by the OnNew callbacks if (NewUnread) { SetInt(FIELD_UNREAD, GetInt(FIELD_UNREAD) + NewUnread); LArray a; a.Add(this); Store->OnChange(_FL, a, FIELD_UNREAD); } */ if (Moved.Length()) { // This allows the UI to display them as "Moving..." Store->OnChange(_FL, Moved, 0); } return Status; } /* Store3CopyImpl(ImapFolder) { ImapFolder *f = CastFld(&p); if (f) { SetStr(FIELD_FOLDER_NAME, f->GetStr(FIELD_FOLDER_NAME)); SetInt(FIELD_FOLDER_THREAD, f->GetInt(FIELD_FOLDER_THREAD)); SetInt(FIELD_FOLDER_PERM_READ, f->GetInt(FIELD_FOLDER_PERM_READ)); SetInt(FIELD_FOLDER_PERM_WRITE, f->GetInt(FIELD_FOLDER_PERM_WRITE)); SetInt(FIELD_SORT, f->GetInt(FIELD_SORT)); SetInt(FIELD_FOLDER_OPEN, f->GetInt(FIELD_FOLDER_OPEN)); SetInt(FIELD_UNREAD, f->GetInt(FIELD_UNREAD)); SetInt(FIELD_FOLDER_TYPE, f->GetInt(FIELD_FOLDER_TYPE)); } return true; } */ Store3CopyImpl(ImapFolder) { SetStr(FIELD_FOLDER_NAME, p.GetStr(FIELD_FOLDER_NAME)); SetInt(FIELD_SORT, p.GetInt(FIELD_SORT)); SetInt(FIELD_FOLDER_TYPE, p.GetInt(FIELD_FOLDER_TYPE)); SetInt(FIELD_UNREAD, p.GetInt(FIELD_UNREAD)); SetInt(FIELD_FOLDER_INDEX, p.GetInt(FIELD_FOLDER_INDEX)); SetInt(FIELD_FOLDER_OPEN, p.GetInt(FIELD_FOLDER_OPEN)); SetInt(FIELD_FOLDER_THREAD, p.GetInt(FIELD_FOLDER_THREAD)); SetInt(FIELD_FOLDER_PERM_READ, p.GetInt(FIELD_FOLDER_PERM_READ)); SetInt(FIELD_FOLDER_PERM_WRITE, p.GetInt(FIELD_FOLDER_PERM_WRITE)); Field.DeleteObjects(); ImapFolder *Folder = dynamic_cast(&p); if (Folder) { LDataIterator &it = Folder->Fields(); Folder->Field.State = Store3Loaded; for (LDataPropI *f = it.First(); f; f = it.Next()) { Field.Insert(new ImapFolderFld( GetStore(), this, (int)f->GetInt(FIELD_ID), (int)f->GetInt(FIELD_WIDTH))); } } return true; } LString ImapFolder::MakeImapPath(char *From) { LString Ret; if (From) { size_t CacheLen = strlen(Store->GetCache()); size_t FromLen = strlen(From); if (CacheLen >= FromLen) { LAssert(!"From path too short"); } else { Ret = From + CacheLen; Ret = Ret.Replace("\\", "/"); } } return Ret; } uint64 ImapFolder::Size() { return 0; } void ImapFolder::ScanFolder() { if (ScanState == 0) { ScanState = 1; LDirectory d; for (int b = d.First(Local); b; b = d.Next()) { if (d.IsDir()) Folders.New() = d.GetName(); } } } void ImapFolder::LoadSub(bool All) { if (Sub.State == Store3Unloaded) { Sub.State = Store3Loading; // Look at the local folders... ScanFolder(); for (unsigned i=0; iSetParent(this); } else LgiTrace("%s:%i - alloc err\n", _FL); } else LgiTrace("%s:%i - '%s' can't make path\n", _FL, s); } if (All) Fld->LoadSub(true); } // uint64 End = LCurrentTime(); // LgiTrace("LoadSub '%s' Time: %i, Items: %i\n", Remote.Get(), (int)(End-Start), Items); Sub.State = Store3Loaded; } } struct ImapFolderLoadThread : public LThread, public ImapFolderData { ImapFolder *f; LString Local, Remote; LArray PostDel; ImapFolderLoadThread(ImapFolder *folder) : f(folder), LThread("ImapFolderLoadThread") { Swap(*f); Local = f->Local.Get(); Remote = f->Remote.Get(); Run(); } void LoadMail() { } void SetDirty(bool b = true) { } int Main() { // auto StartTs = LCurrentTime(); // In same cases like an "OnNew" event mail can show up in a folder // before we load it. That is allowed but we have to de-duplicate it // here, so create a hash table of existing entries and don't create // new ImapMail objects for them in the lower loop LHashTbl, ImapMail*> Map; for (unsigned i=0; iUid != 0); if (m->Uid) Map.Add(m->Uid, m); } auto startTs = LCurrentTime(); ssize_t pos = 0; for (auto it: UidMap) { #if IMAP_PROTOBUF LAssert(0); #else auto ServerId = it.value->GetAsInt(ATTR_UID); if (ServerId && !it.value->GetContent() && Local) { char p[MAX_PATH_LEN], leaf[32]; sprintf_s(leaf, sizeof(leaf), "%i.eml", ServerId); LMakePath(p, sizeof(p), Local, leaf); // Files may not exist if they haven't downloaded yet... it.value->SetContent(leaf); } if (it.value->GetContent() && ServerId) { // Check the format of the msgid, old versions of Scribe used to // leave new-lines in the text... this fixes that on the fly. char *MsgId = it.value->GetAttr(ATTR_MSGID); if (MsgId && strchr(MsgId, '\n')) { LAutoString a(TrimStr(MsgId, " \t\r\n")); it.value->SetAttr(ATTR_MSGID, a); f->SetDirty(); } // Load an email from the cache file if it's not already in the // Mail array. Check there is no existing object: ImapMail *m = Map.Find(ServerId); if (!m) { // No? Well create one: m = new ImapMail(f->Store, _FL, ServerId); if (m) { m->Data = this; m->Parent = f; if (!AddMail(m, NULL)) { DeleteObj(m); } } else if (m->RemoteFlags.ImapDeleted) { PostDel.Add(m); } } } else LAssert(!"Invalid tag..."); #endif pos++; auto now = LCurrentTime(); if (now - startTs >= 500) { startTs = now; if (f->Mail.Prog) f->Mail.Prog(pos, UidMap.Length()); } } for (auto del: PostDel) DelMail(del); // LgiTrace("ImapFolderLoadThread::Main(%s): " LPrintfInt64 "ms.\n", // Remote.Get(), LCurrentTime() - StartTs); auto Store = f->GetStore(); LAutoPtr msg(new ImapMsg(IMAP_LOAD_FOLDER, _FL)); auto &fi = msg->Fld.New(); fi.Local = Local.Get(); Store->GetEvents()->Post(Store, msg.Release()); return 0; } }; void ImapFolder::LoadMail() { if (Mail.State != Store3Unloaded) return; // We really do not want to be creating a second load thread, while one is already running. LAssert(!LoadThread); LoadThread.Reset(new ImapFolderLoadThread(this)); // This happens after the thread creation, because the thread steals the state data // from the Mail container using a Swap. Mail.State = Store3Loading; // Wait and see if it loads quickly: auto StartTs = LCurrentTime(); while ( LCurrentTime() - StartTs < TIMEOUT_FOLDER_LOAD && !LoadThread->IsExited()) LSleep(1); // LgiTrace("ImapFolder::LoadMail(%s) threaded=%i\n", Remote.Get(), !LoadThread->IsExited()); if (LoadThread->IsExited()) { // Yes it did... finish the processing: OnLoadMail(false); } } void ImapFolder::OnLoadMail(bool Threaded) { if (Mail.State == Store3Loaded) return; if (LoadThread) { // auto StartTs = LCurrentTime(); // Return all the data to the main folder ImapFolderData::Swap(*LoadThread.Get()); // NULL out the data ptrs for (auto m: Mail.a) m->Data = NULL; LoadThread.Reset(); //LgiTrace("ImapFolder::OnLoadMail(%s) prcessing: " LPrintfInt64 "ms.\n", // Remote.Get(), LCurrentTime() - StartTs); } else LAssert(!"Where is the thread?"); if (_Parent) { ImapMsg *m = new ImapMsg(IMAP_FOLDER_LISTING, _FL); if (m) { ImapFolderInfo &i = m->Fld.New(); i.Remote = Remote.Get(); i.Local = Local.Get(); i.LastUid = GetLastUid(); Store->PostThread(m, false); } } // Set this collection to loaded even if we get updates from the // server later. We can update the UI through the OnNew/OnDelete // events. Mail.State = Store3Loaded; // Call any onload callbacks... for (auto &cb: OnLoad) cb(Store3Delayed); OnLoad.Empty(); if (Threaded) { // Tell the UI thread about this... it'll need to update the screen LArray Items; Items.Add(this); Store->OnChange(_FL, Items, FIELD_STATUS); } } Store3Status ImapFolder::WhenLoaded(std::function cb) { if (Mail.State == Store3Loaded) { cb(Store3Success); return Store3Success; } // Save the callback for later... OnLoad.Add(cb); if (Mail.State == Store3Unloaded) LoadMail(); // Start the loading process... return Store3Delayed; } Store3Status ImapFolder::Save(LDataI *Into) { if (!IsOnDisk() && Into) { if (!LeafName) { LAssert(!"No leaf name set yet."); return Store3Error; } ImapFolder *f = dynamic_cast(Into); if (f) { f->LoadSub(); char s[MAX_PATH_LEN]; LMakePath(s, sizeof(s), f->Local, LeafName); Local = s; Remote = MakeImapPath(Local); if (LDirExists(Local)) { LAssert(!"Sub-folder already exists."); Remote.Empty(); Local.Empty(); return Store3Error; } FileDev->CreateFolder(Local); ImapMsg *m = new ImapMsg(IMAP_CREATE_FOLDER, _FL); if (!m) return Store3Error; ImapFolderInfo &Inf = m->Fld.New(); Inf.Local = Local.Get(); Inf.Remote = Remote.Get(); Store->PostThread(m, false); SetParent(f); Field.State = Store3Loaded; } else { LAssert(!"Not a valid parent folder."); return Store3Error; } } if (Mail.State != Store3Loaded) { // First read the state off disk so we don't lose anything... LAssert(!"State needs to be loaded before saving."); return Store3Error; } return Serialize(true) ? Store3Success : Store3Error; } Store3Status ImapFolder::Delete(bool ToTrash) { ImapMsg *m = new ImapMsg(IMAP_DELETE_FOLDER, _FL); if (m) { ImapFolderInfo &Inf = m->Fld.New(); Inf.Local = Local.Get(); Inf.Remote = Remote.Get(); if (Store->PostThread(m, false)) return Store3Delayed; } return Store3Error; } class ImapDownload : public LStream { LVariant Path; LFile *f; public: ImapDownload(char *p) { Path = p; f = 0; } ~ImapDownload() { DeleteObj(f); } int Open(const char *Str, int Int) { LAssert(0); return false; } bool IsOpen() { return f ? f->IsOpen() : 0; } int Close() { LAssert(0); return false; } int64 GetSize() { return f ? f->GetSize() : -1; } int64 SetSize(int64 Size) { return f ? f->SetSize(Size) : -1; } int64 GetPos() { return f ? f->GetPos() : -1; } int64 SetPos(int64 Pos) { return f ? f->SetPos(Pos) : -1; } ssize_t Read(void *Buffer, ssize_t Size, int Flags = 0) { return f ? f->Read(Buffer, Size, Flags) : 0; } ssize_t Write(const void *Buffer, ssize_t Size, int Flags = 0) { if (!f) { if ((f = new LFile)) { if (!f->Open(Path.Str(), O_WRITE)) { LgiTrace("%s:%i - Error: Failed to open IMAP file '%s'\n", _FL, Path.Str()); } } } return f ? f->Write(Buffer, Size, Flags) : 0; } }; #if !IMAP_PROTOBUF int IdxCmp(LXmlTag *a, LXmlTag *b, int d) { char *aa = a->GetAttr(ATTR_UID); char *bb = b->GetAttr(ATTR_UID); LAssert(aa && bb); return atoi(aa) - atoi(bb); } #endif static const char *Ws = " \t\r\n"; #define Skip(s) while (*s && strchr(Ws, *s)) s++ char *ImapTok(char *&s) { Skip(s); if (*s == '\"' || *s == '\'') { char d = *s++; char *Start = s; char *End = s; for (; *End; End++) { if (*End == d) break; if (*End == '\\') End++; } if (!End) return 0; s = End + 1; return NewStr(Start, End - Start); } else if (*s == '{') { s++; char *e = strchr(s, '}'); if (e) { int Size = atoi(s); s = e + 1; Skip(s); char *Str = NewStr(s, Size); s += Size; return Str; } else { LAssert(!"Parse error"); return 0; } } else { char *Start = s; while (*s && !strchr(" \r\t\n)", *s)) s++; return NewStr(Start, s - Start); } } struct ImapPair { LAutoString Name; LAutoString Value; }; bool SkipStructure(char *&s) { int Depth = 0; while (s && *s) { if (*s == '(') { s++; Depth++; } else if (*s == ')') { if (Depth == 0) break; s++; Depth--; } else if (*s == '\"' || *s == '\'') { char delim = *s++; while (*s && *s != delim) s++; if (*s == delim) s++; } else s++; } return true; } bool ImapParseStructure(LXmlTag *t, char *&s) { LXmlTag *c = new LXmlTag("Seg"); if (!c) return false; t->InsertTag(c); char m[256]; while (s && *s && *s != ')') { // Check for nesting... Skip(s); if (*s == '(') { // Read nested segment(s) s++; if (!ImapParseStructure(c, s)) return false; Skip(s); if (*s == ')') { // Scan past the end marker s++; } else { LgiTrace("Error: Parsing IMAP structure - wrong token after sub\n"); return false; } } else // Read token stream that represents a MIME segment { LArray p; while (s && *s && *s != ')') { Skip(s); if (*s == '(') { // Named values s++; Skip(s); while (s && *s && *s != ')') { ImapPair &v = p.New(); v.Name.Reset(ImapTok(s)); Skip(s); if (*s == '(') { // Sublist of values... argh. // This might need to be recursive? // ssize_t DelIndex = (ssize_t)p.Length()-1; s++; Skip(s); while (s && *s && *s != ')') { ImapPair &vv = p.New(); LAutoString Name(ImapTok(s)); sprintf_s(m, sizeof(m), "%s(%s)", v.Name.Get(), Name.Get()); vv.Name.Reset(NewStr(m)); Skip(s); if (*s == ')') { LgiTrace("Error: Parsing IMAP structure - wrong token in named value\n"); return false; } vv.Value.Reset(ImapTok(s)); Skip(s); } if (*s == ')') { s++; } else { LgiTrace("Error: Parsing IMAP structure - wrong token after named value\n"); return false; } Skip(s); v.Name.Reset(); continue; } else if (*s == ')') { // Move the name to the value... a single token is always just a value. v.Value = v.Name; } else { v.Value.Reset(ImapTok(s)); } Skip(s); } if (*s == ')') { s++; } else { LgiTrace("Error: Parsing IMAP structure - wrong token after named value\n"); return false; } } else { // Unnamed value ImapPair &v = p.New(); v.Value.Reset(ImapTok(s)); if (p.Length() == 7 && !_stricmp(p[0].Value, "message")) { if (!SkipStructure(s)) return false; } } } unsigned i = 0; if (p.Length() > 1) { if (c->Children.Length()) { ImapPair &a = p[i++]; if (a.Value) { sprintf_s(m, sizeof(m), "multipart/%s", a.Value.Get()); if (!c->SetAttr("mimetype", StrLwr(m))) return false; } } else { ImapPair &a = p[i++]; ImapPair &b = p[i++]; if (a.Value && b.Value) { sprintf_s(m, sizeof(m), "%s/%s", a.Value.Get(), b.Value.Get()); if (!c->SetAttr("mimetype", StrLwr(m))) return false; } } } while (i < p.Length()) { ImapPair &v = p[i++]; if (v.Name) { if (_stricmp(v.Name, "boundary") && !strchr(v.Name, '*')) { if (!c->SetAttr(v.Name, v.Value)) return false; } } else if (v.Value) { if (i == 6) { if (!c->SetAttr("encoding", v.Value)) return false; } else if (i == 7) { if (!c->SetAttr("size", atoi(v.Value))) return false; } } } } } return true; } struct UidTbl : public LHashTbl,ImapMail*> { UidTbl(DIterator &Mail) { for (unsigned i=0; iUid) { Add(m->Uid, m); } else { // There is no UID in the case that there's a new message // being appended to the IMAP folder and hasn't got a UID // yet... just ignore it. Add the UID to the table when // the IMAP_APPEND message gets back from the worker thread. } } else LAssert(0); } } }; LString ImapFolder::MailPath(uint32_t Uid, bool CheckExists) { char f[32], p[MAX_PATH_LEN]; sprintf_s(f, sizeof(f), "%i.eml", Uid); LMakePath(p, sizeof(p), Local, f); if (CheckExists) { LAssert(!LFileExists(p)); } return p; } #define IMAP_ONLISTING_LOG 0 void ImapFolder::OnListing(ImapMsg *m) { // This code does a replication between the list of mail on the server and the local // database, removing local email that doesn't exist on the server, updating existing // mail and creating new mail for objects only on the server. LArray NewMail, OldMail, DelMail, ChangedMail; // Build a hash of our local mail... UidTbl Tbl(Mail); #if IMAP_ONLISTING_LOG LgiTrace("OnListing(%s)\n", Remote.Get()); #endif ImapFolderInfo &Fld = m->Fld[0]; bool Unclean = false; // Go through the incoming list of mail... for (auto &Inf: m->Mail) { if (!Inf.Uid) { LAssert(!"No UID."); continue; } // Do we have the mail in our local mail? ImapMail *Existing = Tbl.Find(Inf.Uid); if (Existing) { // Already exists. Tbl.Delete(Inf.Uid); // Clear the ref, we've "seen" this email on the server. // Update flags? auto ExistingMeta = UidMap.Find(Inf.Uid); if (!ExistingMeta) { LAssert(!"No meta for this email."); continue; } bool ItemDirty = false; #if IMAP_ONLISTING_LOG bool dTypes[3] = {0}; #endif if (Inf.Size > 0) { #if IMAP_PROTOBUF auto mSize = ExistingMeta->size(); if (mSize != Inf.Size) { ExistingMeta->set_size(Inf.Size); ItemDirty = Unclean = true; #if IMAP_ONLISTING_LOG dTypes[0] = true; #endif } #else char *sSize = ExistingMeta->GetAttr(ATTR_SIZE); if (!sSize || atoi64(sSize) != Inf.Size) { LString s; s = Inf.Size; Unclean |= ExistingMeta->SetAttr(ATTR_SIZE, s); ItemDirty = true; #if IMAP_ONLISTING_LOG dTypes[0] = true; #endif } #endif } #if IMAP_ONLISTING_LOG LString sDate; #endif if (Inf.Date) { LDateTime Dt; if (Dt.Decode(Inf.Date)) { #if IMAP_ONLISTING_LOG auto Tmp = Dt; #endif Dt.ToUtc(); #if IMAP_ONLISTING_LOG sDate.Printf("[%s, %s, %s, %s]", Inf.Date.Get(), Tmp.Get().Get(), Dt.Get().Get(), Existing->DateSent.Get().Get()); #endif if (Dt != Existing->DateSent) { ValidateImapDate(Dt); Existing->DateSent = Dt; #if IMAP_PROTOBUF ExistingMeta->set_datesent(Dt.Ts()); Unclean = true; #else char c[32]; Dt.SetFormat(GDTF_YEAR_MONTH_DAY); Dt.Get(c, sizeof(c)); Unclean |= ExistingMeta->SetAttr(ATTR_DATE, c); #endif ItemDirty = true; #if IMAP_ONLISTING_LOG dTypes[1] = true; #endif } } } #if IMAP_PROTOBUF ImapMailFlags CurFlags; CurFlags.All = ExistingMeta->remoteflags(); #else ImapMailFlags CurFlags(ExistingMeta->GetAttr(ATTR_FLAGS)); #endif if (CurFlags.All != Inf.Flags.All) { if (Inf.Flags.ImapDeleted) { // Becoming deleted... DelMail.Add(Existing); // But don't let it fall into the 'Changed' items.. ItemDirty = false; } else { // Flags changing.. #if IMAP_PROTOBUF ExistingMeta->set_remoteflags(Inf.Flags.All); #else Unclean |= ExistingMeta->SetAttr(ATTR_FLAGS, Inf.Flags.Get()); #endif ItemDirty = true; #if IMAP_ONLISTING_LOG dTypes[2] = true; #endif } } if (ItemDirty) { LAssert(!DelMail.HasItem(Existing)); ChangedMail.Add(Existing); #if IMAP_ONLISTING_LOG LgiTrace("%s:%i %i:%i existing changed %i,%i,%i %s\n", _FL, i, Inf.Uid, dTypes[0], dTypes[1], dTypes[2], sDate.Get()); #endif } } else if (!Inf.Flags.ImapDeleted) { // Doesn't exist, so must be new. ImapMail *New = new ImapMail(Store, _FL, Inf.Uid); if (New) { #if IMAP_ONLISTING_LOG LString sDate; #endif New->Path = MailPath(Inf.Uid, false); if (Inf.Date) { if (New->DateSent.Decode(Inf.Date)) { #if IMAP_ONLISTING_LOG auto Tmp = New->DateSent; #endif New->DateSent.ToUtc(); ValidateImapDate(New->DateSent); #if IMAP_ONLISTING_LOG sDate.Printf("[%s, %s, %s]", Inf.Date.Get(), Tmp.Get().Get(), New->DateSent.Get().Get()); #endif } else { LgiTrace("%s:%i - Date parse failed '%s'\n", _FL, Inf.Date.Get()); New->DateSent.Decode(Inf.Date); } } New->SetRemoteFlags(Inf.Flags); New->Parent = this; bool IsNewMail = /* The problem with using the recent flag for IsNew is that other mail clients can be reading the inbox causing the recent flag to be consumed. This means Scribe sees the unread mail as old and doesn't trigger the required filters. At least that's my understanding. New->RemoteFlags.ImapRecent && */ !New->RemoteFlags.ImapSeen && System == Store3SystemInbox; #if IMAP_ONLISTING_LOG LgiTrace("%s:%i - Imap.IsNewMail %i, %i, %i(%i) '%s'\n", _FL, New->RemoteFlags.ImapRecent, New->RemoteFlags.ImapSeen, System == Store3SystemInbox, System, Inf.Flags.Get()); #endif // auto meta = New->GetMeta(false); AddMail(New, IsNewMail ? &NewMail : &OldMail); auto t = New->GetMeta(); LAssert(t != NULL); #if IMAP_ONLISTING_LOG LgiTrace("%s:%i idx=%i uid=%i new mail %i %s\n", _FL, i, Inf.Uid, IsNewMail, sDate.Get()); #endif if (t) { if (Inf.Size > 0) { #if IMAP_PROTOBUF t->set_size(Inf.Size); Unclean = true; #else LString s; s = Inf.Size; Unclean |= t->SetAttr(ATTR_SIZE, s); #endif } char *Struct = Inf.Structure; #if !IMAP_PROTOBUF if (Struct && !ImapParseStructure(t, Struct)) { #ifdef _DEBUG LFile f; if (f.Open( #ifdef WIN32 "c:\\temp\\" #endif "imap-struct.txt", O_WRITE)) { f.SetSize(0); f.Write(Inf.Structure, strlen(Inf.Structure)); f.Close(); LAssert(!"Imap structure parse failed."); } #endif } #endif } } } } // Now 'Tbl' contains all the local mail NOT on the server... // for (ImapMail *Old = Tbl.First(); Old; Old = Tbl.Next()) for (auto it : Tbl) { auto OldUid = it.value->Uid; // Why is this condition here??? if (Fld.LastUid == 0 || // Ie we are doing a full folder refresh or initial load OldUid > Fld.LastUid) // Or we are updated all the email after Fld.LastUid { #if IMAP_ONLISTING_LOG LgiTrace("%s:%i uid:%i deleted (notify)\n", _FL, it.key); #endif DelMail.Add(it.value); } else { #if IMAP_ONLISTING_LOG LgiTrace("%s:%i uid:%i deleted (no-notify)\n", _FL, it.key); #endif } } // Process all the callback events if (OldMail.Length()) Store->OnNew(_FL, this, OldMail, -1, false); if (NewMail.Length()) Store->OnNew(_FL, this, NewMail, -1, true); if (DelMail.Length() && Store->OnDelete(_FL, this, DelMail)) { DelMail.DeleteObjects(); Unclean = true; } if (ChangedMail.Length()) Store->OnChange(_FL, ChangedMail, 0); if (m->Last) Mail.State = Store3Loaded; if (Unclean) SetDirty(); IsOnline = true; } void ImapFolder::Swap(ImapFolder &f) { LAssert(WriteThread.Get() == NULL); // Probably can't swap this. LSwap(LastIdleSelect, f.LastIdleSelect); LSwap(IsOnline, f.IsOnline); LSwap(ScanState, f.ScanState); LSwap(Dirty, f.Dirty); LSwap(Store, f.Store); LSwap(System, f.System); LSwap(ItemType, f.ItemType); auto p1 = GetParent(); SetParent(f.GetParent()); f.SetParent(p1); #if IMAP_PROTOBUF LSwap(ThreadView, f.ThreadView); LSwap(PermRead, f.PermRead); LSwap(PermWrite, f.PermWrite); LSwap(SortField, f.SortField); LSwap(Expanded, f.Expanded); LSwap(UnreadCount, f.UnreadCount); #else Meta.Swap(f.Meta); #endif RootName.Swap(f.RootName); Folders.Swap(f.Folders); Files.Swap(f.Files); UidMap.Swap(f.UidMap); Mail.Swap(f.Mail); Remote.Swap(f.Remote); Local.Swap(f.Local); LeafName.Swap(f.LeafName); Sub.Swap(f.Sub); Field.Swap(f.Field); } int ImapFolder::GetLastUid() { uint32_t Max = 0; for (auto it : UidMap) Max = MAX(Max, it.key); return Max; } void ImapFolder::OnSelect(bool b) { if (Mail.State == Store3Loaded) { ImapMsg *m = new ImapMsg(IMAP_SELECT_FOLDER, _FL); if (m) { ImapFolderInfo &i = m->Fld.New(); i.Remote = Remote.Get(); i.Local = Local.Get(); i.LastUid = GetLastUid(); Store->PostThread(m, false); } } } void ImapFolder::OnDeleteComplete() { LgiTrace("OnDeleteComplete %s\n", Local.Get()); // Clear out child folders... while (Sub.Length()) { ImapFolder *f = CastFld(Sub.First()); if (f) { f->OnDeleteComplete(); DeleteObj(f); } else break; } // Delete all the mail files.. while (Mail.Length()) { ImapMail *m = Mail.a[0]; if (m) { m->OnDelete(); DeleteObj(m); } else break; } // Remove ourselves from the parent folder... SetParent(NULL); if (Local) { FileDev->RemoveFolder(Local, true); } } LAutoStreamI ImapFolder::GetStream(const char *file, int line) { LAssert(0); return LAutoStreamI(0); } bool ImapFolder::SetStream(LAutoStreamI stream) { LAssert(0); return false; } const char *ImapFolder::GetStr(int id) { switch (id) { case FIELD_FOLDER_NAME: { if (IsRoot()) { if (!RootName && Store->SettingStore) { LVariant v; if (Store->SettingStore->GetValue(OPT_AccountName, v)) { RootName = v.Str(); } } if (!RootName) { char *User = Store->User.Str(); char *At = User ? strchr(User, '@') : 0; RootName.Printf("%.*s@%s", (int)(At ? At - User : strlen(User)), Store->User.Str(), Store->Host.Str()); } return RootName; } char *d = strrchr(Local, DIR_CHAR); return d ? d + 1 : (char*)"Error"; } } LAssert(0); return NULL; } Store3Status ImapFolder::SetStr(int id, const char *str) { switch (id) { case FIELD_FOLDER_NAME: { return WhenLoaded([this, str](auto Status) { if (IsOnDisk()) { LAutoPtr RenameMsg(new ImapMsg(IMAP_RENAME_FOLDER, _FL)); if (!RenameMsg) return Store3Error; LString l = Local.Get(); ssize_t last = l.RFind(DIR_STR); if (last) l.Length(last); char s[MAX_PATH_LEN]; LMakePath(s, sizeof(s), l, str); RenameMsg->Parent = Remote.Get(); RenameMsg->NewRemote = MakeImapPath(s); Store->PostThread(RenameMsg.Release(), true); return Store3Delayed; } else { LeafName = str; return Store3Success; } }); break; } } LAssert(0); return Store3Error; } #define GetI(p) \ return Meta.GetAsInt(p); \ break; int64 ImapFolder::GetInt(int id) { switch (id) { case FIELD_SYSTEM_FOLDER: return System; case FIELD_IS_ONLINE: return IsOnline || GetParent() == NULL; case FIELD_STORE_TYPE: return Store3Imap; case FIELD_FOLDER_THREAD: #if IMAP_PROTOBUF return ThreadView; #else if (Meta.GetAsInt(PROP_THREAD) < 0) Meta.SetAttr(PROP_THREAD, 0); GetI(PROP_THREAD); #endif case FIELD_FOLDER_PERM_READ: #if IMAP_PROTOBUF return PermRead; #else GetI(PROP_READ); #endif case FIELD_FOLDER_PERM_WRITE: #if IMAP_PROTOBUF return PermWrite; #else GetI(PROP_WRITE); #endif case FIELD_SORT: #if IMAP_PROTOBUF return SortField; #else if (SortCache == INVALID_CACHE) SortCache = Meta.GetAsInt(PROP_SORT); return SortCache; #endif case FIELD_FOLDER_OPEN: #if IMAP_PROTOBUF return Expanded; #else GetI(PROP_EXPANDED); #endif case FIELD_UNREAD: #if IMAP_PROTOBUF return UnreadCount; #else GetI(PROP_UNREAD); #endif case FIELD_FOLDER_TYPE: return ItemType; case FIELD_LOADED: return Mail.State == Store3Loaded; case FIELD_FOLDER_INDEX: return -1; } LAssert(0); return -1; } #define SetI(p) \ Meta.SetAttr(p, (int)i); \ SetDirty(true); \ return Store3Success; \ Store3Status ImapFolder::SetInt(int id, int64 i) { switch (id) { case FIELD_FOLDER_THREAD: return WhenLoaded([this, i](auto Status) { #if IMAP_PROTOBUF ThreadView = i; return Store3Success; #else SetI(PROP_THREAD); #endif }); case FIELD_FOLDER_PERM_READ: return WhenLoaded([this, i](auto Status) { #if IMAP_PROTOBUF PermRead = (ScribePerm)i; return Store3Success; #else SetI(PROP_READ); #endif }); case FIELD_FOLDER_PERM_WRITE: return WhenLoaded([this, i](auto Status) { #if IMAP_PROTOBUF PermWrite = (ScribePerm)i; return Store3Success; #else SetI(PROP_WRITE); #endif }); case FIELD_SORT: return WhenLoaded([this, i](auto Status) { #if IMAP_PROTOBUF SortField = i; return Store3Success; #else SortCache = (int)i; SetI(PROP_SORT); #endif }); case FIELD_FOLDER_OPEN: return WhenLoaded([this, i](auto Status) { #if IMAP_PROTOBUF Expanded = i != 0; return Store3Success; #else SetI(PROP_EXPANDED); #endif }); case FIELD_UNREAD: return WhenLoaded([this, i](auto Status) { #if IMAP_PROTOBUF UnreadCount = i; return Store3Success; #else SetI(PROP_UNREAD); #endif }); case FIELD_FOLDER_TYPE: return WhenLoaded([this, i](auto Status) { ItemType = (int)i; }); break; case FIELD_FOLDER_INDEX: return Store3Error; case FIELD_LOADED: { if (i) { LoadMail(); } else { // Unload folder... Mail.DeleteObjects(); Mail.State = Store3Unloaded; } break; } default: LAssert(0); return Store3Error; } return Store3Success; } const LDateTime *ImapFolder::GetDate(int id) { LAssert(0); return NULL; } Store3Status ImapFolder::SetDate(int id, const LDateTime *i) { LAssert(0); return Store3Error; } LDataPropI *ImapFolder::GetObj(int id) { LAssert(0); return NULL; } LDataIt ImapFolder::GetList(int id) { LAssert(0); return NULL; } LDataIterator &ImapFolder::SubFolders() { LoadSub(); return Sub; } LDataIterator &ImapFolder::Children() { LoadMail(); return Mail; } LDataIterator &ImapFolder::Fields() { return Field; } Store3Status ImapFolder::FreeChildren() { if (Store) { LArray Lst; for (unsigned i=0; iUserData) Lst.Add(t); } if (!Store->Callback->OnDelete(this, Lst)) return Store3Error; } Sub.DeleteObjects(); UidMap.DeleteObjects(); Mail.DeleteObjects(); Mail.State = Store3Unloaded; return Store3Success; } Store3Status ImapFolder::DeleteAllChildren() { // Delete cmd LAutoPtr Msg(new ImapMsg(IMAP_DELETE, _FL)); Msg->Parent = Remote.Get(); if (!Store->PostThread(Msg.Release(), false)) return Store3Error; // Immediately expunge Msg.Reset(new ImapMsg(IMAP_EXPUNGE_FOLDER, _FL)); Msg->Fld[0].Remote = Remote.Get(); if (!Store->PostThread(Msg.Release(), false)) return Store3Error; return Store3Delayed; } ImapFolder *ImapFolder::Find(const char *local, const char *remote) { if (Local.Equals(local) || Remote.Equals(remote)) return this; LoadSub(); for (auto cf: Sub.a) { LAssert(cf->GetParent() == this); ImapFolder *r = cf->Find(local, remote); if (r) return r; } return NULL; } ImapMail *ImapFolder::FindByFile(char *File) { if (!File) return 0; for (unsigned i=0; iPath) { if (!_stricmp(m->Path, File)) return m; } } return 0; } ImapMail::IMeta ImapFolderData::GetMeta(uint32_t id, bool Create) { if (!id) return NULL; auto t = UidMap.Find(id); #if IMAP_PROTOBUF if (!t && Create && (t = new scribe::Email)) { t->set_uid(id); UidMap.Add(id, t); } #else if (!t && Create && (t = new LXmlTag(TAG_MAIL))) { t->SetAttr(ATTR_UID, (int)id); UidMap.Add(id, t); } #endif return t; } bool ImapFolderData::DeleteMeta(uint32_t id) { if (!id) return false; auto t = UidMap.Find(id); if (!t) return false; #ifdef _DEBUG ImapMail *m = GetMail(id); LAssert(m == 0); if (m) DelMail(m); #endif UidMap.Delete(id); DeleteObj(t); return true; } ImapMail *ImapFolderData::GetMail(uint32_t Uid) { if (!Uid) return 0; for (auto m: Mail.a) { if (m->Uid == Uid) return m; } return 0; } bool ImapFolderData::DelMail(ImapMail *m) { if (!m) { LAssert(!"Null ptr."); return false; } LoadMail(); if (Mail.IndexOf(m) < 0) { LAssert(!"Doesn't exist."); return false; } auto t = UidMap.Find(m->Uid); if (t) { #if 0 // def _DEBUG LStackTrace("%s DelMail %s\n", Remote.Get(), m->Uid.Get()); #endif UidMap.Delete(m->Uid); DeleteObj(t); } else { static bool First = true; if (First) { First = false; LAssert(!"There really should be a meta tag."); } } Mail.Delete(m); SetDirty(); return true; } #define DEBUG_PROF_ADD_MAIL 0 #if DEBUG_PROF_ADD_MAIL #define PROFILE(str) Prof.Add(str) #else #define PROFILE(str) #endif bool ImapFolderData::AddMail(ImapMail *m, LArray *OnNew) { #if DEBUG_PROF_ADD_MAIL LProfile Prof("ImapFolder::AddMail", 1); #endif if (!m) { LAssert(!"Null ptr."); return false; } PROFILE("Index chk"); if (Mail.IndexOf(m, true) >= 0) { LAssert(!"Already added."); return false; } PROFILE("Mail insert"); if (!m->Parent) { m->Parent = dynamic_cast(this); LAssert(m->Parent); } Mail.Insert(m, -1, true); if (m->Uid) { #if IMAP_PROTOBUF // UidMap.Add(m->Uid, m); #else PROFILE("GetMeta"); LXmlTag *t = m->GetMeta(false); if (!t && (t = m->GetMeta())) { // Creating a new meta tag... set it's variables. PROFILE("Set attr"); t->SetAttr(ATTR_UID, (int)m->Uid); // UidMap will already have the entry by now... PROFILE("Serialize"); m->Serialize(t, true); } else { PROFILE("Serialize"); m->Serialize(t, false); } #endif } // else its a temp object for appending to a mail box PROFILE("DIR chk"); char *Leaf = m->Path ? strrchr(m->Path, DIR_CHAR) : 0; if (!Leaf) { LAssert(!"Not a valid path."); } if (OnNew) { PROFILE("OnNewAdd"); OnNew->Add(m); } return true; } void ImapFolder::OnPulse() { // int64 Now = LCurrentTime(); if (Dirty) { Save(); } if (Sub.GetState() == Store3Loaded) { for (LDataFolderI *c=Sub.First(); c; c=Sub.Next()) { ImapFolder *f = dynamic_cast(c); f->OnPulse(); } } } void ImapFolder::OnCommand(const char *Name) { if (!Remote) return; ScribeDomType Method = StrToDom(Name); switch (Method) { case SdExpunge: { ImapMsg *m = new ImapMsg(IMAP_EXPUNGE_FOLDER, _FL); m->Fld[0].Remote = Remote.Get(); Store->PostThread(m, false); break; } case SdUndelete: { ImapMsg *m = new ImapMsg(IMAP_UNDELETE, _FL); m->Parent = Remote.Get(); for (auto it : UidMap) { #if IMAP_PROTOBUF LAssert(0); #else char *Flags = it.value->GetAttr(ATTR_FLAGS); if (Flags && stristr(Flags, "/deleted")) { ImapMailInfo &Inf = m->Mail.New(); Inf.Uid = it.key; } #endif } Store->PostThread(m, false); break; } case SdRefresh: { ImapMsg *m = new ImapMsg(IMAP_FOLDER_LISTING, _FL); ImapFolderInfo &f = m->Fld[0]; f.Remote = Remote.Get(); f.LastUid = 0; Store->PostThread(m, false); break; } default: { LAssert(!"Unknown command"); break; } } } void ImapFolder::OnExpunge() { LArray Del; for (auto it : UidMap) { #if IMAP_PROTOBUF LAssert(0); #else char *Flags = it.value->GetAttr(ATTR_FLAGS); if (Flags && stristr(Flags, "/deleted")) Del.Add(it.value); #endif } for (unsigned i=0; iGetAsInt(ATTR_UID); LAssert(Uid > 0); if (Uid) { UidMap.Delete(Uid); DeleteObj(Del[i]); } #endif } } bool ImapFolder::OnRename(const char *NewRemote) { if (!NewRemote || !GetParent()) { LAssert(!"Param error."); return false; } char s[MAX_PATH_LEN]; LMakePath(s, sizeof(s), Store->Cache, NewRemote); if (!FileDev->Move(Local, s)) { LAssert(!"Failed to move folder."); return false; } Local = s; Remote = NewRemote; return true; } /////////////////////////////////////////////////////////////////////////////////// ImapFolderFld::ImapFolderFld(LDataStoreI *s, ImapFolder *p, int id, int width) : LXmlTag("Field") { Parent = p; SetAttr("Id", id); SetAttr("Width", width); } const char *ImapFolderFld::GetStr(int id) { if (id == FIELD_NAME) { switch (GetAsInt("Id")) { case FIELD_CACHE_FILENAME: return "Filename"; case FIELD_CACHE_FLAGS: return "ImapFlags"; } } return 0; } int64 ImapFolderFld::GetInt(int id) { switch (id) { case FIELD_ID: return GetAsInt("Id"); case FIELD_WIDTH: return GetAsInt("Width"); } return -1; } Store3Status ImapFolderFld::SetInt(int id, int64 i) { switch (id) { case FIELD_ID: SetAttr("Id", (int)i); break; case FIELD_WIDTH: SetAttr("Width", (int)i); break; default: return Store3Error; } if (Parent) Parent->SetDirty(); return Store3Success; } diff --git a/src/Store3Imap/ScribeImap_Mail.cpp b/src/Store3Imap/ScribeImap_Mail.cpp --- a/src/Store3Imap/ScribeImap_Mail.cpp +++ b/src/Store3Imap/ScribeImap_Mail.cpp @@ -1,1489 +1,1489 @@ #include "lgi/common/Lgi.h" -#include "lgi/common/NetTools.h" +#include "lgi/common/LgiRes.h" +#include "lgi/common/TextConvert.h" + #include "ScribeImap.h" #include "ScribeUtils.h" #include "resdefs.h" -#include "lgi/common/LgiRes.h" -#include "lgi/common/TextConvert.h" #if IMAP_PROTOBUF #include "include/Scribe.pb.cc" #endif ImapMail::ImapMail(ImapStore *store, const char *file, int line, uint32_t uid) : AllocFile(file), AllocLine(line), From(store), Reply(store) { LocalFlags = 0; Seg = 0; State = ImapMailIdle; Loaded = Store3Unloaded; Parent = 0; SegDirty = false; RemoteFlags.ImapSeen = true; Priority = MAIL_PRIORITY_NORMAL; Store = store; Uid = uid; } ImapMail::~ImapMail() { if (Parent) { Parent->DelMail(this); } DeleteObj(Seg); To.DeleteObjects(); } void ImapMail::SetRemoteFlags(ImapMailFlags f) { RemoteFlags = f; // Replicate to local flags as well... if (f.ImapSeen) LocalFlags |= MAIL_READ; else LocalFlags &= ~MAIL_READ; if (f.ImapAnswered) LocalFlags |= MAIL_REPLIED; else LocalFlags &= ~MAIL_REPLIED; } ImapMail::IMeta ImapMail::GetMeta(bool AllowCreate) { if (Data) return Data->GetMeta(Uid, AllowCreate); if (Parent) return Parent->GetMeta(Uid, AllowCreate); return NULL; } void ImapMail::Serialize(IMeta m, bool Write) { LAssert(m != NULL); if (!m) return; #if IMAP_PROTOBUF if (Write) { } else { } #else if (Write) { // Mail -> Meta m->SetAttr(ATTR_FLAGS, RemoteFlags.Get()); if (DateSent.IsValid()) { char c[32]; DateSent.SetFormat(GDTF_YEAR_MONTH_DAY); DateSent.ToUtc(); DateSent.Get(c, sizeof(c)); DateSent.SetFormat(DateSent.GetDefaultFormat()); ValidateImapDate(DateSent); m->SetAttr(ATTR_DATE, c); } m->SetAttr(ATTR_LABEL, Label); m->SetAttr(ATTR_COLOUR, (int64)Colour.c32()); char *l = Path ? strrchr(Path, DIR_CHAR) : 0; if (l) m->SetContent(l + 1); if (ValidStr(Subject)) m->SetAttr(ATTR_SUBJECT, Subject); else m->DelAttr(ATTR_SUBJECT); LVariant v; if (From.GetVariant("Text", v)) m->SetAttr(ATTR_FROM, v.Str()); if (Reply.GetVariant("Text", v)) m->SetAttr(ATTR_REPLYTO, v.Str()); Parent->SetDirty(); } else { // Meta -> Mail auto Flags = m->GetAttr(ATTR_FLAGS); if (Flags) RemoteFlags.Set(Flags); char *sLocal = m->GetAttr(ATTR_LOCAL); if (sLocal) LocalFlags = atoi(sLocal); else if (Flags) // Synthesis from remote flags LocalFlags = (RemoteFlags.ImapAnswered ? MAIL_REPLIED : 0) | (RemoteFlags.ImapSeen ? MAIL_READ : 0); char *s = m->GetAttr(ATTR_DATE); DateSent.SetFormat(GDTF_YEAR_MONTH_DAY); DateSent.SetTimeZone(0, false); DateSent.Set(s); DateSent.SetFormat(DateSent.GetDefaultFormat()); // Hmmmm: still don't know where these are coming from if (DateSent.Year() == 2000 && DateSent.Hours() == 0) { DateSent.Empty(); if (Parent && !Parent->IsLoading()) Parent->SetDirty(); } DateReceived = DateSent; Label = m->GetAttr(ATTR_LABEL); char *c = m->GetAttr(ATTR_COLOUR); if (c) Colour.Set((uint32_t)atoi64(c), 32); else Colour.Empty(); Subject = m->GetAttr(ATTR_SUBJECT); LVariant FromText = m->GetAttr(ATTR_FROM); #if 1 From.SetVariant("Text", FromText); #else char *sFrom = FromText.Str(); if (sFrom) { LRange Nm(-1), Em(-1); for (char *c = sFrom; *c; c++) { if (*c == '\"') if (Nm.Start < 0) Nm.Start = c - sFrom + 1; else Nm.Len = c - sFrom - Nm.Start; else if (*c == '<') Em.Start = c - sFrom + 1; else if (*c == '>') Em.Len = c - sFrom - Em.Start; } if (Nm.Len) { if (Nm.Start >= 0) From.Name.Set(sFrom + Nm.Start, Nm.Len); else LAssert(0); } if (Em.Len) { if (Em.Start >= 0) From.Addr.Set(sFrom + Em.Start, Em.Len); else LAssert(0); } } #endif if (Parent && m->GetContent()) { // Trip whitespace of the end of the filename... char *e = m->GetContent() + strlen(m->GetContent()); while (e > m->GetContent() && strchr(" \t\r\n", e[-1])) e--; *e = 0; char p[MAX_PATH_LEN]; LMakePath(p, sizeof(p), Parent->Local, m->GetContent()); Path = p; } } #endif } void ImapMail::SetState(ImapMailState s) { /* if (s == ImapMailGettingBody) LgiTrace("%s:%i - SetStart ImapMailGettingBody for %i\n", _FL, Uid); */ State = s; } void ImapMail::Load() { if (Loaded == Store3Unloaded) { LFile f; if (LFileExists(Path) && f.Open(Path, O_READ)) { Loaded = Store3Headers; LArray Buf; int Chunk = 2 << 10; while (!HeaderCache) { Buf.Length(Buf.Length() + Chunk); f.Read(&Buf[Buf.Length()-Chunk], Chunk); auto e = Strnstr(&Buf[0], "\r\n\r\n", Buf.Length()); if (e) { ssize_t HeaderSize = e - &Buf[0]; HeaderCache.Reset(NewStr(&Buf[0], HeaderSize)); break; } if ((int64)Buf.Length() >= f.GetSize()) break; } LAutoString Type(InetGetHeaderField(HeaderCache, "Content-Type")); if (Type) { if (_strnicmp(Type, "multipart/", 10) == 0 && _strnicmp(Type, sMultipartAlternative, 21) != 0) { SetInt(FIELD_FLAGS, LocalFlags | MAIL_ATTACHMENTS); } } } } } Store3Status ImapMail::SetRfc822(LStreamI *m) { DeleteObj(Seg); if (!Stream.Reset(new LTempStream(ScribeTempPath()))) { LgiTrace("%s:%i - Alloc failed.\n", _FL); return Store3Error; } LCopyStreamer Cp; if (!Cp.Copy(m, Stream)) { LgiTrace("%s:%i - LCopyStreamer copy failed.\n", _FL); return Store3Error; } Loaded = Store3Loaded; return Store3Success; } #define DEBUG_READ_MIME 0 void ImapMail::ReadMime(IMeta MetaTag) { #if DEBUG_READ_MIME LProfile Prof("ReadMime"); #endif if (Loaded == Store3Loaded || !Uid) return; auto t = MetaTag ? MetaTag : GetMeta(); if (!t) { LAssert(!"No Meta"); return; } #if DEBUG_READ_MIME Prof.Add(_FL); #endif if (!LFileExists(Path)) { if (State != ImapMailGettingBody) { /*bool InParent =*/ Parent && Parent->GetMail(Uid); // We don't have a local copy of the message, tell the thread to fetch it... ImapMsg *Msg = new ImapMsg(IMAP_DOWNLOAD, _FL); if (Msg) { #if DEBUG_READ_MIME Prof.Add(_FL); #endif ImapMailInfo &Info = Msg->Mail.New(); #if IMAP_PROTOBUF Info.Size = t->size(); #else Info.Size = t->GetAsInt(ATTR_SIZE); #endif Info.Uid = Uid; Info.Local = Path.Get(); Msg->Parent = Parent->Remote.Get(); if (Store->PostThread(Msg, true)) { SetState(ImapMailGettingBody); Loaded = Store3Loading; } else LgiTrace("%s:%i - PostThread failed.\n", _FL); } } } else { // Load the rest of the mime parts Loaded = Store3Loaded; #if DEBUG_READ_MIME Prof.Add(_FL); #endif LStreamI *Load = NULL; LFile f; if (Stream) { Load = Stream; } else if (f.Open(Path, O_READ)) { Load = &f; } #if DEBUG_READ_MIME Prof.Add(_FL); #endif if (Load) { if (Load->GetSize() < 8) { // Hmmm, really? Try and re-download the mail ImapMsg *Msg = new ImapMsg(IMAP_DOWNLOAD, _FL); if (Msg) { Loaded = Store3Headers; #if DEBUG_READ_MIME Prof.Add(_FL); #endif ImapMailInfo &Info = Msg->Mail.New(); #if IMAP_PROTOBUF Info.Size = t->size(); Info.Uid = t->uid(); #else Info.Size = t->GetAsInt(ATTR_SIZE); Info.Uid = t->GetAsInt(ATTR_UID); #endif Info.Local = Path.Get(); Msg->Parent = Parent->Remote.Get(); Store->PostThread(Msg, true); } } else { LAutoPtr Mime(new LMime); if (Mime) { uint64 Start = LCurrentTime(); #if DEBUG_READ_MIME Prof.Add(_FL); #endif Mime->Text.Decode.Pull(Load); DeleteObj(Seg); #if DEBUG_READ_MIME Prof.Add(_FL); #endif Seg = new ImapAttachment(Store, this, Mime.Release()); #if DEBUG_READ_MIME Prof.Add(_FL); #endif if (Seg->IsMixed()) SetInt(FIELD_FLAGS, LocalFlags | MAIL_ATTACHMENTS); uint64 Dur = LCurrentTime() - Start; if (Dur > 100) LgiTrace("%s:%i - MimeDecode: " LPrintfInt64 "\n", _FL, Dur); } } } } } bool ImapMail::FindSegs(const char *Type, LArray &Segs) { if (!Seg) ReadMime(NULL); if (Seg) { Seg->FindSegs(Type, Segs); for (unsigned i=0; iGetSeg()->GetFileName()); if (Fn) { Segs.DeleteAt(i--, true); } } } return Segs.Length() > 0; } static auto DefaultCharset = "windows-1252"; const char *ImapMail::GetStr(int id) { if (State != ImapMailIdle && id != FIELD_INTERNET_HEADER && id != FIELD_MESSAGE_ID && id != FIELD_SERVER_UID) { if (id != FIELD_SUBJECT) return NULL; // LgiTrace("%s:%i - State=%i, Uid=%i(0x%x)\n", _FL, State, Uid, Uid); if (State == ImapMailGettingBody) { static const char *Loading = NULL; if (!Loading) Loading = LLoadString(IDS_LOADING); return Loading; } else if (State == ImapMailDeleting) { static const char *Deleting = NULL; if (!Deleting) Deleting = LLoadString(IDS_DELETING); return Deleting; } else if (State == ImapMailMoving) { return (char*)"Moving..."; } return NULL; } switch (id) { case FIELD_DEBUG: { static char s[64]; sprintf_s(s, sizeof(s), "Imap.ServerUid=%i", Uid); return s; } case FIELD_INTERNET_HEADER: { if (Seg && Seg->GetSeg()) return Seg->GetSeg()->GetHeaders(); if (Path) { if (LFileExists(Path)) { Load(); } else if (State == ImapMailIdle) { // Request the headers from the worker thread... ImapMsg *Msg = new ImapMsg(IMAP_DOWNLOAD, _FL); if (Msg) { auto t = GetMeta(); ImapMailInfo &Info = Msg->Mail.New(); #if IMAP_PROTOBUF Info.Size = t ? t->size() : -1; Info.Uid = t ? t->uid() : 0; #else Info.Size = t ? t->GetAsInt(ATTR_SIZE) : -1; Info.Uid = Uid; #endif Info.Local = Path.Get(); Msg->Parent = Parent->Remote.Get(); if (Store->PostThread(Msg, false)) { SetState(ImapMailGettingBody); Loaded = Store3Loading; } else LgiTrace("%s:%i - PostThread failed.\n", _FL); } } } return HeaderCache; } case FIELD_MESSAGE_ID: { if (!MsgId) { auto m = GetMeta(); if (m) #if IMAP_PROTOBUF MsgId = m->messageid().c_str(); #else MsgId = m->GetAttr(ATTR_MSGID); #endif if (!MsgId) { auto Headers = GetStr(FIELD_INTERNET_HEADER); LAutoString h(DecodeRfc2047(InetGetHeaderField(Headers, "Message-ID"))); MsgId = h; if (MsgId) { LAssert(!strchr(MsgId, '\n')); if (m) { #if IMAP_PROTOBUF m->set_messageid(MsgId); #else m->SetAttr(ATTR_MSGID, MsgId); #endif Parent->SetDirty(); } } } } return MsgId; } case FIELD_SUBJECT: { if (!Subject) { auto Headers = GetStr(FIELD_INTERNET_HEADER); if (!Headers) return LLoadString(IDS_LOADING); LAutoString s(DecodeRfc2047(InetGetHeaderField(Headers, "Subject"))); Subject = s; auto Meta = GetMeta(); if (Meta) Serialize(Meta, true); } return Subject; } case FIELD_CHARSET: { // Conversion is done locally. return "utf-8"; } case FIELD_TEXT: { LArray Results; if (!TextCache && FindSegs("text/plain", Results)) { LStringPipe p; for (auto Segment: Results) { if (Segment->GetInt(FIELD_SIZE) > (512 << 10)) continue; LAutoStreamI Stream = Segment->GetStream(_FL); if (Stream) { LAutoString Buf; auto Size = Stream->GetSize(); if (Size > 0) { Buf.Reset(new char[Size+1]); auto rd = Stream->Read(Buf, Size); if (rd <= 0) continue; Buf.Get()[rd] = 0; LString Charset = Segment->GetStr(FIELD_CHARSET); if (!Charset) Charset = DetectCharset(Buf.Get()); if (!Charset) Charset = DefaultCharset; LAutoString Utf8; if (!Stricmp(Charset.Get(), "utf-8") && LIsUtf8(Buf)) Utf8 = Buf; else Utf8.Reset((char*)LNewConvertCp("utf-8", Buf, Charset, rd)); if (Results.Length() > 1) { p.Push(Utf8); } else { TextCache = Utf8; return TextCache; } } } } TextCache.Reset(p.NewStr()); } return TextCache; break; } case FIELD_HTML_CHARSET: { LArray Results; if (FindSegs("text/html", Results)) return Results[0]->GetStr(FIELD_CHARSET); break; } case FIELD_ALTERNATE_HTML: { LArray Results; if (!HtmlCache && FindSegs("text/html", Results)) { LAutoStreamI s = Results[0]->GetStream(_FL); if (s) { auto Size = s->GetSize(); if (Size > 0) { HtmlCache.Reset(new char[Size+1]); s->Read(HtmlCache, Size); HtmlCache[Size] = 0; } } } return HtmlCache; break; } case FIELD_CACHE_FLAGS: { static char s[96]; int ch = 0; if (RemoteFlags.ImapAnswered) ch += sprintf_s(s+ch, sizeof(s)-ch, "Answ "); if (RemoteFlags.ImapDeleted) ch += sprintf_s(s+ch, sizeof(s)-ch, "Del "); if (RemoteFlags.ImapDraft) ch += sprintf_s(s+ch, sizeof(s)-ch, "Drft "); if (RemoteFlags.ImapFlagged) ch += sprintf_s(s+ch, sizeof(s)-ch, "Flg "); if (RemoteFlags.ImapRecent) ch += sprintf_s(s+ch, sizeof(s)-ch, "Rec "); if (RemoteFlags.ImapSeen) ch += sprintf_s(s+ch, sizeof(s)-ch, "Seen "); if (RemoteFlags.ImapExpunged) ch += sprintf_s(s+ch, sizeof(s)-ch, "Exp "); return s; break; } case FIELD_CACHE_FILENAME: { char *l = Path ? strrchr(Path, DIR_CHAR) : 0; return l ? l + 1 : Path.Get(); } case FIELD_SERVER_UID: { // We allow the app to get the integer UID here as a string // because POP3 UID's ARE strings, so we need to be compatible if (!UidCache) UidCache.Printf("%u", Uid); return UidCache; break; } case FIELD_LABEL: { auto m = GetMeta(); #if IMAP_PROTOBUF return m ? m->label().c_str() : NULL; #else return m ? m->GetAttr(ATTR_LABEL) : NULL; #endif break; } } return NULL; } void CopyImapSegs(ImapMail *Mail, LDataPropI *Dst, LDataPropI *Src) { if (!Dst || !Src) { LAssert(!"Invalid ptrs"); return; } LDataI *DDst = dynamic_cast(Dst); LDataI *DSrc = dynamic_cast(Src); if (!DDst || !DSrc) { LAssert(!"Cast failed."); return; } DDst->CopyProps(*DSrc); LDataIt DstLst = Dst->GetList(FIELD_MIME_SEG); LDataIt SrcLst = Src->GetList(FIELD_MIME_SEG); ImapAttachment *ParentAtt = dynamic_cast(Dst); LMime *ParentMime = ParentAtt->GetSeg(); for (LDataPropI *c = SrcLst->First(); c; c = SrcLst->Next()) { LDataPropI *n = DstLst->Create(Mail->GetStore()); if (n) { CopyImapSegs(Mail, n, c); ImapAttachment *ChildAtt = dynamic_cast(n); ChildAtt->AttachTo(ParentAtt); LMime *ChildMime = ChildAtt->GetSeg(); if (ParentMime && ChildMime) { if (!ParentMime->Insert(ChildMime)) { LAssert(!"Insert failed."); } } } else LAssert(!"Create object failed."); } } Store3CopyImpl(ImapMail) { Priority = (int)p.GetInt(FIELD_PRIORITY); SetInt(FIELD_FLAGS, p.GetInt(FIELD_FLAGS)); SetStr(FIELD_LABEL, p.GetStr(FIELD_LABEL)); int64 Col = p.GetInt(FIELD_COLOUR); SetInt(FIELD_COLOUR, Col); MsgId = p.GetStr(FIELD_MESSAGE_ID); Subject = p.GetStr(FIELD_SUBJECT); LDataPropI *i = p.GetObj(FIELD_FROM); if (i) From.CopyProps(*i); i = p.GetObj(FIELD_REPLY); if (i) Reply.CopyProps(*i); const LDateTime *d = p.GetDate(FIELD_DATE_RECEIVED); if (d) DateReceived = *d; else DateReceived.Empty(); d = p.GetDate(FIELD_DATE_SENT); if (d) { DateSent = *d; ValidateImapDate(DateSent); } else DateSent.Empty(); To.DeleteObjects(); LDataIt pTo = p.GetList(FIELD_TO); for (unsigned n=0; nLength(); n++) { Store3Addr *a = new Store3Addr(GetStore(), (*pTo)[n]); if (a) To.Insert(a, -1, true); } To.State = Store3Loaded; Seg = dynamic_cast(GetObj(FIELD_MIME_SEG)); if (!Seg) Seg = new ImapAttachment(Store, this); auto SrcSeg = p.GetObj(FIELD_MIME_SEG); if (SrcSeg) CopyImapSegs(this, Seg, SrcSeg); return true; } #define _Str(v) { char *s = NewStr(str); DeleteArray(v); v = s; return !((s != 0) ^ (str != 0)); } Store3Status ImapMail::SetStr(int id, const char *str) { Load(); switch (id) { case FIELD_SUBJECT: Subject = str; break; case FIELD_MESSAGE_ID: { // Set the local copy MsgId = str; // Set the folder meta data copy auto m = GetMeta(); if (m) { #if IMAP_PROTOBUF m->set_messageid(str); #else m->SetAttr(ATTR_MSGID, str); #endif Parent->SetDirty(); } break; } case FIELD_LABEL: { auto m = GetMeta(); if (m) { #if IMAP_PROTOBUF m->set_label(str); #else m->SetAttr(ATTR_LABEL, Label = str); #endif Parent->SetDirty(); } else { Label = str; } break; } case FIELD_SERVER_UID: // ?? break; case FIELD_INTERNET_HEADER: { if (HeaderCache.Get() != str) HeaderCache.Reset(NewStr(str)); // Reset all the cached values... MsgId.Empty(); Subject.Empty(); From.Empty(); Reply.Empty(); DateReceived.Empty(); DateSent.Empty(); To.DeleteObjects(); DeleteObj(Seg); Loaded = Store3Unloaded; break; } case FIELD_ALTERNATE_HTML: { const char *Mt; ImapAttachment *Alt = Seg ? Seg->FindChildByMimeType(sMultipartAlternative) : NULL; ImapAttachment *Html = Alt ? Alt->FindChildByMimeType(sTextHtml) : NULL; if (Html) { if (str) { // Set new stream LAutoStreamI a(new LMemStream(str, strlen(str))); if (Html->SetStream(a)) SegDirty = true; } else { // Delete content Html->Delete(); delete Html; break; } } else if (Seg && (Mt = Seg->GetStr(FIELD_MIME_TYPE)) && !_stricmp(Mt, sTextHtml)) // Is the root segment HTML? { // Set the data to an empty stream... LAutoStreamI a(str ? new LMemStream(str, strlen(str)) : NULL); if (Seg->SetStream(a)) SegDirty = true; } HtmlCache.Reset(); break; } } return Store3Success; } int64 ImapMail::GetInt(int id) { switch (id) { case FIELD_STORE_TYPE: return Store3Imap; case FIELD_LOADED: return Loaded; case FIELD_DONT_SHOW_PREVIEW: return true; case FIELD_SIZE: { auto Meta = GetMeta(); if (Meta) { #if IMAP_PROTOBUF return DataSize = Meta->size(); #else char *Sz = Meta->GetAttr(ATTR_SIZE); if (Sz) return atoi64(Sz); #endif } DataSize = LFileSize(Path); LAssert(DataSize <= 0x7fffffff); return DataSize; } case FIELD_PRIORITY: return Priority; case FIELD_FLAGS: // LgiTrace("%p.LocalFlags = %s\n", this, EmailFlagsToStr(LocalFlags).Get()); return LocalFlags; case FIELD_COLOUR: { uint32_t col = Colour.c32(); auto m = GetMeta(); if (m) { #if IMAP_PROTOBUF col = m->colour(); #else char *s = m->GetAttr(ATTR_COLOUR); if (s) col = (uint32_t)atoi64(s); #endif } return col; break; } case FIELD_ACCOUNT_ID: return Store->GetInt(id); case FIELD_SERVER_UID: return Uid; } return -1; } Store3Status ImapMail::SetInt(int id, int64 i) { Load(); switch (id) { case FIELD_LOADED: { if (Loaded >= i) return Store3Success; if (i == Store3Loaded) ReadMime(NULL); else LAssert(!"What other option is there?"); if (Loaded >= i) return Store3Success; if (Loaded == Store3Loading) return Store3Delayed; return Store3Error; // what happened here? break; } case FIELD_COLOUR: { Colour.Set((uint32_t)i, 32); auto m = GetMeta(); if (m) { #if IMAP_PROTOBUF m->set_colour(i); #else m->SetAttr(ATTR_COLOUR, i); #endif Parent->SetDirty(); } break; } case FIELD_PRIORITY: Priority = (int)i; break; case FIELD_FLAGS: { bool Seen = RemoteFlags.ImapSeen; bool Answered = RemoteFlags.ImapAnswered; LocalFlags = (int)i; RemoteFlags.ImapSeen = TestFlag(i, MAIL_READ); RemoteFlags.ImapAnswered = TestFlag(i, MAIL_REPLIED); if (!Parent) return Store3Success; auto t = GetMeta(); if (t) { #if IMAP_PROTOBUF t->set_remoteflags(RemoteFlags.All); t->set_localflags(RemoteFlags.All); #else t->SetAttr(ATTR_FLAGS, RemoteFlags.Get()); t->SetAttr(ATTR_LOCAL, i); #endif Parent->SetDirty(); } else { LAssert(!"No tag."); return Store3Error; } if ( (RemoteFlags.ImapSeen ^ Seen) || (RemoteFlags.ImapAnswered ^ Answered) ) { // Update the IMAP server LAutoPtr Msg(new ImapMsg(IMAP_SET_FLAGS, _FL)); if (!Msg) return Store3Error; Msg->Parent = Parent->Remote.Get(); LAssert(Msg->Parent); ImapMailInfo &Info = Msg->Mail.New(); Info.Uid = Uid; Info.Flags = RemoteFlags; if (!Store->PostThread(Msg.Release(), false)) return Store3Error; return Store3Delayed; // caller doesn't have to set the object dirty } return Store3Success; } } return Store3Success; } const LDateTime *ImapMail::GetDate(int id) { if ((id == FIELD_DATE_RECEIVED && !DateReceived.Year()) || (id == FIELD_DATE_SENT && !DateSent.Year())) { /* auto m = GetMeta(false); if (m) { auto s = m->GetAttr(ATTR_DATE); int asd=0; } */ auto Headers = GetStr(FIELD_INTERNET_HEADER); LAutoString h(InetGetHeaderField(Headers, "Date")); if (h) { DateReceived.Decode(h); DateReceived.ToUtc(); DateSent = DateReceived; ValidateImapDate(DateSent); } } switch (id) { case FIELD_DATE_RECEIVED: return &DateReceived; case FIELD_DATE_SENT: return &DateSent; } return 0; } Store3Status ImapMail::SetDate(int id, const LDateTime *i) { Load(); switch (id) { case FIELD_DATE_RECEIVED: DateReceived = *i; break; case FIELD_DATE_SENT: DateSent = *i; ValidateImapDate(DateSent); break; default: return Store3Error; } return Store3Success; } Store3Addr *ImapMail::ProcessAddress(Store3Addr &Addr, const char *FieldId, const char *RfcField) { if (!Addr.Addr) { // Try and load from the meta... auto m = GetMeta(); if (m) { #if IMAP_PROTOBUF auto f = m->from(); if (!f.email().empty()) { Addr.SetStr(FIELD_NAME, f.name().c_str()); Addr.SetStr(FIELD_EMAIL, f.email().c_str()); return &Addr; } #else auto f = m->GetAttr(FieldId); if (f) { LAutoString Name, Email; DecodeAddrName(f, Name, Email, 0); Addr.SetStr(FIELD_NAME, Name); Addr.SetStr(FIELD_EMAIL, Email); return &Addr; } #endif } // Otherwise try and load from the headers... auto Headers = GetStr(FIELD_INTERNET_HEADER); LAutoString f(DecodeRfc2047(InetGetHeaderField(Headers, RfcField))); if (f) { LAutoString Name, Email; DecodeAddrName(f, Name, Email, 0); Addr.SetStr(FIELD_NAME, Name); Addr.SetStr(FIELD_EMAIL, Email); if (Uid) Serialize(m, true); } } return &Addr; } LDataPropI *ImapMail::GetObj(int id) { switch (id) { case FIELD_MIME_SEG: { if (!Seg) ReadMime(NULL); return Seg; } case FIELD_FROM: { return ProcessAddress(From, ATTR_FROM, "From"); } case FIELD_REPLY: { return ProcessAddress(Reply, ATTR_REPLYTO, "Reply-To"); } } return 0; } Store3Status ImapMail::SetObj(int id, LDataPropI *i) { LAssert(0); return Store3Error; } LDataIt ImapMail::GetList(int id) { Load(); switch (id) { case FIELD_TO: { if (!To.Length()) { auto Headers = GetStr(FIELD_INTERNET_HEADER); LAutoString h(DecodeRfc2047(InetGetHeaderField(Headers, "To"))); if (h) { List Addr; TokeniseStrList(h, Addr, ","); for (auto a: Addr) { Store3Addr *la = new Store3Addr(GetStore()); if (la) { DecodeAddrName(a, la->Name, la->Addr, 0); To.Insert(la, -1, true); } } Addr.DeleteArrays(); } To.State = Store3Loaded; if (h.Reset(DecodeRfc2047(InetGetHeaderField(Headers, "Cc")))) { List Addr; TokeniseStrList(h, Addr, ","); for (auto a: Addr) { Store3Addr *la = new Store3Addr(GetStore()); if (la) { DecodeAddrName(a, la->Name, la->Addr, 0); la->CC = true; To.Insert(la); } } Addr.DeleteArrays(); } } return &To; } } return 0; } Store3Status ImapMail::Save(LDataI *Folder) { Store3Status Status = Store3Error; ImapFolder *Fld = dynamic_cast(Folder); if (!Fld) { LAssert(0); LgiTrace("%s:%i - No folder.\n", _FL); } else if (Path) { LAssert(0); LgiTrace("%s:%i - Already has path?\n", _FL); } else { if (!Seg && !Stream) { // Need to create an empty text segment LMime *m = new LMime(ScribeTempPath()); if (!m) return Store3Error; m->SetMimeType(sTextPlain); if (Subject) m->Set("Subject", Subject); if (DateSent.IsValid()) m->Set("Date", DateSent.Get()); if (MsgId) m->Set("MessageID", MsgId); if (From.Addr) { LVariant v; if (From.GetValue("Text", v)) m->Set("From", v.Str()); } if (Reply.Addr) { LVariant v; if (Reply.GetValue("Text", v)) m->Set("Reply", v.Str()); } Seg = new ImapAttachment(Store, this, m); } char r[32], p[MAX_PATH_LEN]; do { sprintf_s(r, sizeof(r), "temp_%u.eml", LRand()); LMakePath(p, sizeof(p), Fld->Local, r); } while (LFileExists(p)); Path = p; LFile f; if (f.Open(Path, O_WRITE)) { bool SaveFailed = false; if (Stream) { // Copy the import stream directly to disk... LCopyStreamer Cp; Stream->SetPos(0); if (Cp.Copy(Stream, &f)) { // After writing to disk, this is not needed anymore. Stream.Release(); } else { LgiTrace("%s:%i - Copy failed.\n", _FL); SaveFailed = true; } } else if (Seg) { // Re-encode the MIME segment tree... auto s = Seg->GetSeg(); if (!s) { LgiTrace("%s:%i - No segment.\n", _FL); SaveFailed = true; } else if (!s->Text.Encode.Push(&f)) { LgiTrace("%s:%i - Mime encode failed.\n", _FL); SaveFailed = true; } } f.Close(); if (!SaveFailed) { LAutoPtr m(new ImapMsg(IMAP_APPEND, _FL)); m->Parent = Fld->Remote.Get(); m->Mail[0].Local = Path.Get(); m->Mail[0].Flags = RemoteFlags; if (Fld->Store->PostThread(m.Release(), false)) { Status = Store3Delayed; Fld->LoadMail(); Fld->AddMail(this, 0); } else { LgiTrace("%s:%i - PostThread failed.\n", _FL); } } } else { LgiTrace("%s:%i - Failed to open '%s' for writing.\n", _FL, Path.Get()); } } return Status; } Store3Status ImapMail::Delete(bool ToTrash) { Store3Status Status = Store3Error; LAutoPtr m(new ImapMsg(IMAP_DELETE, _FL)); m->Mail[0].Uid = Uid; m->Parent = Parent->Remote.Get(); if (Store->PostThread(m.Release(), false)) { SetState(ImapMailDeleting); Status = Store3Delayed; LArray a; a.Add(this); Store->OnChange(_FL, a, 0); } return Status; } bool ImapMail::OnDelete() { bool Status = false; if (Parent) { Parent->DelMail(this); Parent = 0; } if (Path) { Status = FileDev->Delete(Path, false); } return Status; } LAutoStreamI ImapMail::GetStream(const char *file, int line) { LAutoStreamI Ret; if (Stream) { Ret.Reset(new LProxyStream(Stream)); } else { LFile *f; if (Ret.Reset(f = new LFile)) { if (!f->Open(Path, O_READWRITE)) Ret.Reset(); } } return Ret; } bool ImapMail::SetStream(LAutoStreamI stream) { if (!stream) return false; DeleteObj(Seg); // MIME parse the stream and store it to segments. LAutoPtr Mime(new LMime); if (Mime->Text.Decode.Pull(stream)) { Seg = new ImapAttachment(Store, this, Mime.Release()); if (Seg) { // This stops the objects being written to disk. // Which would mean we have multiple copies of the same // data on disk. This setting also propagates down the // tree automatically as GMimeToStore3 saves new child // notes to their parents. Seg->SetInMemoryOnly(true); Seg->AttachTo(this); } } return false; } void ImapMail::OnDownload(LAutoString &Headers) { LFile f; if (f.Open(Path, O_WRITE)) { f.Write(Headers, strlen(Headers)); f.Close(); } HeaderCache = Headers; Loaded = Store3Headers; SetState(ImapMailIdle); auto t = GetMeta(); if (t) { // t->SetAttr(ATTR_HEADER_ONLY, 1); // This pulls all the relevant fields out of the headers so they can be written // to the meta element. This means it gets loaded without having to touch the // email on disk. Which means faster filtering on the commonly displayed list // fields. GetStr(FIELD_SUBJECT); GetObj(FIELD_FROM); GetDate(FIELD_DATE_SENT); Serialize(t, true); } } diff --git a/src/Store3Imap/ScribeImap_Store.cpp b/src/Store3Imap/ScribeImap_Store.cpp --- a/src/Store3Imap/ScribeImap_Store.cpp +++ b/src/Store3Imap/ScribeImap_Store.cpp @@ -1,1313 +1,1313 @@ #include "lgi/common/Lgi.h" -#include "lgi/common/NetTools.h" + #include "ScribeImap.h" #include "ScribeUtils.h" #if IMAP_PROTOBUF #ifdef _DEBUG #pragma comment(lib, "libprotobufd.lib") #else #pragma comment(lib, "libprotobufd.lib") #endif #endif const char *ImapMsgTypeNames[IMAP_MSG_MAX] = { "IMAP_NULL", // Connection "IMAP_ONLINE", "IMAP_OFFLINE", // Lifespan events "IMAP_ON_NEW", "IMAP_ON_DEL", "IMAP_ON_MOVE", // Folder "IMAP_SELECT_FOLDER", "IMAP_CREATE_FOLDER", "IMAP_DELETE_FOLDER", "IMAP_EXPUNGE_FOLDER", "IMAP_RENAME_FOLDER", // Mail "IMAP_FOLDER_SELECTED", "IMAP_FOLDER_LISTING", "IMAP_DOWNLOAD", "IMAP_APPEND", "IMAP_MOVE_EMAIL", // General "IMAP_DELETE", "IMAP_UNDELETE", "IMAP_SET_FLAGS", }; ////////////////////////////////////////////////////////////////////////////////// char *TrimWhite(char *c) { if (!c) return 0; char *s = c; while (*s && strchr(WhiteSpace, *s)) s++; char *e = s + strlen(s); while (e > s && strchr(WhiteSpace, e[-1])) e--; *e = 0; if (s > c) memmove(c, s, e-s+1); return c; } ////////////////////////////////////////////////////////////////////////////////// ImapStore::ImapStore(char *host, int port, char *user, char *pass, int flags, LDataEventsI *callback, LCapabilityClient *caps, MailProtocolProgress *prog[2], LStream *log, int accountid, LAutoPtr store) { Root = NULL; Cache = 0; Online = STATUS_OFFLINE; LastPulse = 0; ListingTime = SelectTime = 0; AccountId = accountid; Host = host; Port = port; User = user; Pass = pass; ConnectFlags = flags; Log = log; Callback = callback; ItemProgress = prog[0]; DataProgress = prog[1]; SettingStore = store; Thread = new ImapThread(this, caps, log, SettingStore); } ImapStore::~ImapStore() { FolderLoader.Reset(); DeleteObj(Thread); DeleteObj(Root); DeleteArray(Cache); } bool ImapStore::OnIdle() { if (Listing.Length() > 0) { LAutoPtr m(Listing[0]); Listing.DeleteAt(0, true); return Listing.Length() > 0; } int64 Now = LCurrentTime(); if (!LastPulse || (Now - LastPulse) > TIMEOUT_STORE_ONPULSE) { LastPulse = Now; if (Root) Root->OnPulse(); } return false; } const char *ImapStore::GetStr(int id) { switch (id) { case FIELD_ERROR: return ErrorMsg; case FIELD_STORE_TYPE: return "ImapStore"; } return NULL; } int64 ImapStore::GetInt(int id) { switch (id) { case FIELD_STORE_TYPE: return Store3Imap; case FIELD_VERSION: return 10; case FIELD_ACCOUNT_ID: return AccountId; case FIELD_IS_ONLINE: return Online == STATUS_ONLINE; case FIELD_PROFILE_IMAP_LISTING: return ListingTime; case FIELD_PROFILE_IMAP_SELECT: return SelectTime; case FIELD_STORE_STATUS: return Online; } return -1; } Store3Status ImapStore::SetInt(int id, int64 val) { if (id == FIELD_IS_ONLINE && !val) { DeleteObj(Thread); Online = STATUS_OFFLINE; return Store3Success; } return Store3Error; } ImapFolder *ImapStore::GetSystemFolder(int Type) { if (!Root) return NULL; Store3SystemFolder t = Store3SystemNone; switch (Type) { case FOLDER_INBOX: t = Store3SystemInbox; break; case FOLDER_OUTBOX: t = Store3SystemOutbox; break; case FOLDER_SENT: t = Store3SystemSent; break; case FOLDER_TRASH: t = Store3SystemTrash; break; } if (!t) return NULL; for (auto f: Root->Sub.a) { if (f->System == t) return f; } return NULL; } LDataPropI *ImapStore::GetObj(int id) { switch (id) { case FIELD_INBOX: return GetSystemFolder(FOLDER_INBOX); case FIELD_OUTBOX: return GetSystemFolder(FOLDER_OUTBOX); case FIELD_SENT: return GetSystemFolder(FOLDER_SENT); case FIELD_TRASH: return GetSystemFolder(FOLDER_TRASH); } return NULL; } void ImapStore::OnNew(const char *File, int Line, LDataFolderI *parent, LArray &new_items, int pos, bool is_new) { if (!Callback) return; Callback->SetContext(File, Line); Callback->OnNew(parent, new_items, pos, is_new); } bool ImapStore::OnDelete(const char *File, int Line, LDataFolderI *parent, LArray &del) { if (!Callback) return false; Callback->SetContext(File, Line); return Callback->OnDelete(parent, del); } bool ImapStore::OnChange(const char *File, int Line, LArray &items, int FieldHint) { if (!Callback) return false; Callback->SetContext(File, Line); return Callback->OnChange(items, FieldHint); } static bool CheckCreateFolder(char *s, LStream *Log) { if (LDirExists(s)) return true; if (FileDev->CreateFolder(s)) return true; char m[256]; Log->Write(m, sprintf_s(m, sizeof(m), "Can't create '%s'", s), LSocketI::SocketMsgError); return false; } char *ImapStore::GetCache() { if (!Cache) { char s[MAX_PATH_LEN], a[32]; LVariant Portable; LOptionsFile *Opts = Callback->GetOptions(); if (!Opts || !Opts->GetValue(OPT_IsPortableInstall, Portable)) Portable = true; sprintf_s(a, sizeof(a), "%i", AccountId); if (Portable.CastInt32()) LGetSystemPath(LSP_APP_INSTALL, s, sizeof(s)); else LGetSystemPath(LSP_APP_ROOT, s, sizeof(s)); if (!CheckCreateFolder(s, Log)) { LgiTrace("%s:%i - CheckCreateFolder(%s) failed.\n", _FL, s); return NULL; } LMakePath(s, sizeof(s), s, "ImapCache"); if (!CheckCreateFolder(s, Log)) { LgiTrace("%s:%i - CheckCreateFolder(%s) failed.\n", _FL, s); return NULL; } LMakePath(s, sizeof(s), s, a); if (!CheckCreateFolder(s, Log)) { LgiTrace("%s:%i - CheckCreateFolder(%s) failed.\n", _FL, s); return NULL; } Cache = NewStr(s); } return Cache; } uint64 ImapStore::Size() { return -1; } LDataI *ImapStore::Create(int Type) { switch ((uint32_t)Type) { case MAGIC_FOLDER: { return new ImapFolder(this, 0); } case MAGIC_MAIL: { return new ImapMail(this, _FL, 0); } } return 0; } class FolderLoaderThread : public LThread { bool Loop; ImapStore *Store; public: uint64_t StartTs; LAutoPtr Root; FolderLoaderThread(ImapStore *store, const char *base) : LThread("FolderLoaderThread") { Loop = true; Store = store; StartTs = LCurrentTime(); Root.Reset(new ImapFolder(Store, "/")); Run(); } ~FolderLoaderThread() { Loop = false; while (!IsExited()) LSleep(1); } int Main() { Root->LoadSub(true); Store->PostStore(new ImapMsg(IMAP_ONLINE, _FL)); return 0; } }; LDataFolderI *ImapStore::GetRoot(bool create) { if (!GetCache()) return NULL; if (!Root) { Root = new ImapFolder(this, "/"); #if 0 // Start a thread to load the folder tree.. FolderLoader.Reset(new FolderLoaderThread(this, GetCache())); #endif } return Root; } ImapFolder *ImapStore::GetTrash() { ImapFolder *Trash = Root->Find(0, "/Trash"); if (!Trash) Trash = Root->Find(0, "/Deleted Items"); // Should we create a trash folder if it doesn't exist? return Trash; } Store3Status ImapStore::Delete(LArray &Items, bool ToTrash) { if (Items.Length() == 0) return Store3Error; Store3Status Status = Store3Error; LArray Mv, Dl; ImapFolder *Trash = GetTrash(); LAutoPtr DelMsg(new ImapMsg(IMAP_DELETE, _FL)); for (unsigned n=0; n(Items[n]); if (f) { Status = f->Delete(); } else { ImapMail *m = dynamic_cast(Items[n]); if (m) { if (ToTrash && Root) { if (Trash && m->Parent != Trash) { Mv.Add(m); m = NULL; } } if (m) { if (!DelMsg->Parent) DelMsg->Parent = m->Parent->Remote.Get(); // This breaks messages up into blocks of the same parent folder // but no more than IMAP_BLOCK items. if (_stricmp(DelMsg->Parent, m->Parent->Remote) || DelMsg->Mail.Length() >= IMAP_BLOCK_SIZE) { PostThread(DelMsg.Release(), false); DelMsg.Reset(new ImapMsg(IMAP_DELETE, _FL)); DelMsg->Parent = m->Parent->Remote.Get(); Status = Store3Delayed; } Dl.Add(m); m->SetState(ImapMail::ImapMailDeleting); DelMsg->Mail.New().Uid = m->Uid; } } } } if (Trash && Mv.Length()) { Status = Move(Trash, Mv); } if (DelMsg->Mail.Length()) { if (PostThread(DelMsg.Release(), false)) { Status = Store3Delayed; OnChange(_FL, Dl, 0); } } return Status; } Store3Status ImapStore::Move(LDataFolderI *NewFolder, LArray &Items) { Store3Status Status = Store3Error; if (NewFolder) { ImapFolder *To = CastFld(NewFolder); if (To) { Status = To->Move(Items); } else LAssert(!"Not an imap folder."); } return Status; } Store3Status ImapStore::Change(LArray &Items, int PropId, LVariant &Value, LOperator Operator) { switch (PropId) { case FIELD_FLAGS: { uint32_t Val = Value.CastInt32(); LAutoPtr Msg; if (Operator != OpPlusEquals && Operator != OpMinusEquals) { LAssert(!"Operator not supported."); return Store3Error; } for (auto i: Items) { if (!Msg && !Msg.Reset(new ImapMsg(IMAP_SET_FLAGS, _FL))) return Store3Error; ImapMail *m = dynamic_cast(i); if (!m || !m->Parent) continue; auto Fld = m->Parent->Remote; if (Msg->Parent && (Fld != Msg->Parent || Msg->Mail.Length() >= IMAP_BLOCK_SIZE)) { LAssert(Msg->Parent); PostThread(Msg.Release(), false); if (!Msg.Reset(new ImapMsg(IMAP_SET_FLAGS, _FL))) return Store3Error; } if (!Msg->Parent) Msg->Parent = Fld; auto PrevFlags = m->RemoteFlags.All; if (Val & MAIL_READ) m->RemoteFlags.ImapSeen = Operator == OpPlusEquals; else if (Val & MAIL_REPLIED) m->RemoteFlags.ImapAnswered = Operator == OpPlusEquals; else LAssert(!"Unsupported flag."); if (Operator == OpPlusEquals) m->LocalFlags |= Val; else m->LocalFlags &= ~Val; auto t = m->GetMeta(); if (t) { #if IMAP_PROTOBUF t->set_remoteflags(m->RemoteFlags.All); t->set_localflags(m->LocalFlags); #else t->SetAttr(ATTR_FLAGS, m->RemoteFlags.Get()); t->SetAttr(ATTR_LOCAL, m->LocalFlags); #endif m->Parent->SetDirty(); } else LAssert(!"No tag."); if (m->RemoteFlags.All != PrevFlags) { ImapMailInfo &Info = Msg->Mail.New(); Info.Uid = m->Uid; Info.Flags = m->RemoteFlags; } } if (Msg && Msg->Mail.Length() > 0) { LAssert(Msg->Parent); if (!PostThread(Msg.Release(), false)) return Store3Error; } return Store3Delayed; break; } } return Store3Error; } void ImapStore::Compact(LViewI *Parent, LDataPropI *Props, std::function OnStatus) { if (OnStatus) OnStatus(true); } bool ImapStore::PostThread(ImapMsg *m, bool UiPriority) { if (Thread) { Thread->PostThread(m, UiPriority); return true; } // This happens when the imap store is in offline mode. DeleteObj(m); return false; } ImapFolder *ImapStore::GetParent(char *Local, char *Remote) { if (!Root) return 0; LAutoString l(NewStr(Local)), r(NewStr(Remote)); char *s; if (l && (s = strrchr(l, DIR_CHAR))) *s = 0; if (r && (s = strrchr(r, '/'))) *s = 0; return Root->Find(l, r); } void MailInfToObject(LAutoPtr &DownloadMsg, LArray *OnNew, ImapFolder *Parent, ImapMail *o, ImapMailInfo &Inf) { o->Path = Parent->MailPath(Inf.Uid, false); if (Inf.Date) { o->DateSent.Decode(Inf.Date); o->DateSent.ToUtc(); ValidateImapDate(o->DateSent); } // LgiTrace("%s:%i - %p.DateSent = %s\n", _FL, o, Inf.Date.Get()); o->SetRemoteFlags(Inf.Flags); // LgiTrace("MailInfToObject %p -> %s / %s\n", o, Inf.Flags.Get(), EmailFlagsToStr(o->LocalFlags).Get()); o->Parent = Parent; if (o->RemoteFlags.ImapRecent || !LFileExists(o->Path)) { if (!DownloadMsg) { if (DownloadMsg.Reset(new ImapMsg(IMAP_DOWNLOAD, _FL))) DownloadMsg->Parent = Parent->Remote.Get(); } if (DownloadMsg) { ImapMailInfo &Info = DownloadMsg->Mail.New(); Info.Uid = o->Uid; Info.Local = o->Path.Get(); o->SetState(ImapMail::ImapMailGettingBody); } } Parent->WhenLoaded([Parent, o, OnNew](auto Status) { Parent->AddMail(o, Status == Store3Success ? OnNew : NULL); }); } static bool _in_download = false; void ImapStore::OnEvent(void *Param) { if (!Root) return; Msgs.Add((ImapMsg*)Param); while (Msgs.Length()) { int64 Start = LCurrentTime(); ImapMsg *m = Msgs[0]; Msgs.DeleteAt(0, true); switch (m->Type) { default: break; case IMAP_ONLINE: { Online = STATUS_ONLINE; // LProfile p("IMAP_ONLINE"); #if 1 if (Root) Root->LoadSub(); #else if (Root && Root->Sub.GetState() < Store3Loaded) { if (!FolderLoader) { // p.Add("CreateThread"); FolderLoader.Reset(new FolderLoaderThread(this, GetCache())); } else { FolderLoaderThread *t = dynamic_cast(FolderLoader.Get()); if (t && t->Root && t->Root->Sub.GetState() == Store3Loaded) { // p.Add("RootSwap"); Root->Swap(*t->Root); // LgiTrace("FolderLoadTime=" LPrintfInt64 "\n", LCurrentTime() - t->StartTs); } } } #endif // Select the inbox if (Root && Root->Sub.GetState() == Store3Loaded) { // p.Add("Inbox"); // Select the INBOX and start loading it's mail ImapFolder *Inbox = Root->Find(NULL, "/INBOX"); if (Inbox) { // p.Add("Inbox.Select"); Inbox->OnSelect(true); // p.Add("Inbox.Load"); Inbox->LoadMail(); } // p.Add("Callback"); if (Callback) Callback->OnPropChange(this, FIELD_IS_ONLINE, GV_BOOL); } break; } case IMAP_OFFLINE: { Online = STATUS_OFFLINE; if (Callback) Callback->OnPropChange(this, FIELD_IS_ONLINE, GV_BOOL); break; } case IMAP_ERROR: { Online = STATUS_ERROR; break; } case IMAP_ON_NEW: { if (!Root) break; // New folders? LArray NewEvent; for (unsigned i=0; iFld.Length(); i++) { ImapFolderInfo &Inf = m->Fld[i]; ImapFolder *Parent = GetParent(Inf.Local, 0); if (!Parent) { // This happens when there is "Inbox/abc/def" but no // "Inbox/abc". Apparently this is legal for some servers. char sep[] = {Inf.Sep, 0}; LString Sep(sep); LString Unix("/"); LAssert(Inf.Sep != 0); LString::Array a = LString(Inf.Remote).Split(Sep); Parent = Root; for (unsigned i=0; Parent && iFind(NULL, Rem); if (!f) { // Start creating entries... f = new ImapFolder(this); if (!f) break; f->Remote = Rem.Get(); char s[MAX_PATH_LEN]; LMakePath(s, sizeof(s), GetCache(), Rem); f->Local = s; if (!LDirExists(f->Local) && FileDev->CreateFolder(f->Local)) { // Probably should get the server to recreate the folder too? ImapMsg *Create = new ImapMsg(IMAP_CREATE_FOLDER, _FL); if (Create) { ImapFolderInfo &Inf = Create->Fld.New(); Inf.Local = f->Local.Get(); Inf.Remote = f->Remote.Get(); PostThread(Create, false); } } f->SetParent(Parent); NewEvent.Add(f); f->Field.State = Store3Loaded; } if (!f) break; Parent = f; } } if (Parent) { if (!LDirExists(Inf.Local)) { // Create local folder... if (FileDev->CreateFolder(Inf.Local)) { if (Parent->Sub.GetState() != Store3Loaded) { // This will cause the folder to be loaded as if it was pre-existing Parent->LoadSub(); ImapFolder *f = Parent->Find(Inf.Local, NULL); // Get the new folder ptr and pass it to the UI level if (f && Callback) NewEvent.Add(f); } else { LString RemotePath = Parent->MakeImapPath(Inf.Local); ImapFolder *f = new ImapFolder(this, RemotePath); if (f) { f->SetParent(Parent); // LgiTrace("IMAP Store '%s', parent state=%i\n", f->Remote.Get(), Parent->Sub.State); if (Callback) NewEvent.Add(f); } else if (Log) Log->Print("IMAP_ON_NEW: mem alloc failed\n"); } } else { LAssert(!"Failed to create folder."); if (Log) Log->Print("IMAP_ON_NEW: Failed to create folder\n"); } } } } for (unsigned i=0; i Items; Items.Add(NewEvent[i]); ImapFolder *Parent = NewEvent[i]->GetParent(); if (Parent->Sub.State != Store3Loaded) Parent->LoadSub(); Callback->OnNew(Parent, Items, -1, false); } // New email? ImapFolder *Parent = m->Parent ? Root->Find(0, m->Parent) : 0; if (Parent) { LArray Old, Recent; LAutoPtr DownloadMsg; ImapMail *o; for (unsigned i=0; iMail.Length(); i++) { ImapMailInfo &Inf = m->Mail[i]; if (!Inf.Uid) { LAssert(!"No server id."); continue; } bool ParentHasUid = Parent->GetMail(Inf.Uid) != NULL; if (!Inf.Flags.ImapDeleted && !ParentHasUid && (o = new ImapMail(this, _FL, Inf.Uid))) { LArray *a = Inf.Flags.ImapRecent && !Inf.Flags.ImapSeen && Parent->System == Store3SystemInbox ? &Recent : &Old; MailInfToObject(DownloadMsg, a, Parent, o, Inf); } } if (DownloadMsg) { PostThread(DownloadMsg.Release(), false); } if (Callback) { if (Recent.Length()) Callback->OnNew(Parent, Recent, -1, true); if (Old.Length()) Callback->OnNew(Parent, Old, -1, false); } } break; } case IMAP_ON_DEL: { if (!Root) { LAssert(!"No root?"); break; } LgiTrace("IMAP_ON_DEL %i,%i\n", (int)m->Fld.Length(), (int)m->Mail.Length()); // Folders for (unsigned i=0; iFld.Length(); i++) { ImapFolderInfo &Inf = m->Fld[i]; ImapFolder *f = Root->Find(Inf.Local, Inf.Remote); if (f) { LArray Lst; Lst.Add(f); if (Callback->OnDelete(f->GetParent(), Lst)) { f->OnDeleteComplete(); DeleteObj(f); } else LAssert(!"OnDelete failed."); } } // Email ImapFolder *Parent = m->Parent ? Root->Find(0, m->Parent) : 0; if (Parent) { LArray Lst; if (m->Mail.Length() > 0) { for (unsigned i=0; iMail.Length(); i++) { ImapMailInfo &Inf = m->Mail[i]; ImapMail *e = Parent->GetMail(Inf.Uid); if (!e) { LgiTrace("\t%s:%i - Can't find uid '%i' in folder '%s' (msg from %s:%i)\n", _FL, Inf.Uid, m->Parent.Get(), m->File, m->Line); // LAssert(!"No email to delete."); } else if (!Lst.HasItem(e)) { // LgiTrace("%s:%i - Adding %p %s to del lst.\n", _FL, e, e->Uid.Get()); Lst.Add(e); } } } else { for (unsigned i=0; iChildren().Length(); i++) { LDataI *di = Parent->Children()[i]; if (di) Lst.Add(di); } } LgiTrace("\tCallback->OnDelete %i\n", (int)Lst.Length()); if (!Callback || Callback->OnDelete(Parent, Lst)) { for (unsigned i=0; i(Lst[i]); if (e) { #if 0 LgiTrace("\t%s:%i - OnDelete '%s' from folder '%s' (msg from %s:%i)\n", _FL, e->Uid.Get(), Parent->Remote.Get(), m->File, m->Line); #endif LAssert(!_in_download); e->OnDelete(); DeleteObj(e); } } } } // LgiTrace("Ending IMAP_ON_DEL\n"); break; } case IMAP_FOLDER_LISTING: { if (m->Fld.Length() != 1) { LAssert(!"Not the right folder length."); break; } ImapFolder *f = Root->Find(0, m->Fld[0].Remote); if (f) f->OnListing(m); else LAssert(!"No folder"); break; } case IMAP_DOWNLOAD: { ImapFolder *f = Root->Find(0, m->Parent); if (f) { _in_download = true; LArray Changed; for (unsigned i=0; iMail.Length(); i++) { ImapMailInfo &Inf = m->Mail[i]; ImapMail *e = f->GetMail(Inf.Uid); if (e) { e->SetState(ImapMail::ImapMailIdle); auto t = e->GetMeta(); if (t) { e->ReadMime(t); Changed.Add(e); } else { f->Save(); LgiTrace("Error: Can't find meta data for email %s\n", strrchr(e->Path, DIR_CHAR)); LAssert(!"No meta tag for this email?"); } } else { // That can happen if you delete something before the IMAP_DOWNLOAD msg // gets back from the worker thread. } } _in_download = false; f->SetDirty(); if (Changed.Length()) OnChange(_FL, Changed, 0); } else LAssert(!"No folder"); break; } case IMAP_CREATE_FOLDER: { if (Callback && Root) { for (unsigned i=0; iFld.Length(); i++) { ImapFolderInfo &Inf = m->Fld[i]; ImapFolder *f = Root->Find(Inf.Local, Inf.Remote); if (f) { LArray New; New.Add(f); Callback->OnNew(f->GetParent(), New, -1, false); } } } break; } case IMAP_APPEND: { LDataFolderI *NewItemParent = 0; LArray NewItems; ImapFolder *f = Root->Find(0, m->Parent); if (f) { for (unsigned i=0; iMail.Length(); i++) { ImapMailInfo &Inf = m->Mail[i]; ImapMail *m = f->FindByFile(Inf.Local); if (m) { if (Inf.Uid) { // Ok rename the mail object, setup it's new UID m->Uid = Inf.Uid; LString Path = f->MailPath(m->Uid); if (FileDev->Move(m->Path, Path)) { m->Path = Path; } // Create the meta data tag and hook it up auto t = f->GetMeta(m->Uid, true); if (t) { LAssert(m->GetMeta() != NULL); m->Serialize(t, true); if (NewItemParent != NULL && NewItemParent != m->Parent) { // If items from different parents come through we should OnNew them // only when the parent is the same, so flush through ones that have // the old parent folder before we start collecting ones from the new // folder.. if (Callback && NewItems.Length() > 0) Callback->OnNew(NewItemParent, NewItems, -1, false); NewItems.Length(0); } NewItemParent = m->Parent; NewItems.Add(m); if (!m->Seg) { auto Meta = m->GetMeta(); m->Loaded = Store3Unloaded; m->ReadMime(Meta); } } } else { // No uid, just destroy it, we'll get it eventually through the // \recent mail search FileDev->Delete(m->Path, false); m->Path.Empty(); } } } } if (Callback && NewItems.Length()) { // Notify the client the object has arrived. Callback->OnNew(NewItemParent, NewItems, -1, false); } break; } case IMAP_EXPUNGE_FOLDER: { ImapFolder *f = Root->Find(0, m->Parent); if (f) f->OnExpunge(); break; } case IMAP_RENAME_FOLDER: { LArray Renamed; ImapFolder *f = Root->Find(0, m->Parent); if (f && f->OnRename(m->NewRemote)) Renamed.Add(f); OnChange(_FL, Renamed, FIELD_FOLDER_NAME); break; } case IMAP_SET_FLAGS: { LArray Change, Delete, OnNew; ImapFolder *f = Root->Find(0, m->Parent ); if (!f) { LgiTrace("%s:%i - Missing folder '%s'\n", _FL, m->Parent.Get()); break; } LAutoPtr DownloadMsg; #define USE_BINARY_SEARCH 1 // LProfile prof("IMAP_SET_FLAGS"); #if USE_BINARY_SEARCH // Sort the list for BinarySearch //prof.Add("sort"); f->Mail.a.Sort([](auto a, auto b) { return (int32_t)(*a)->Uid - (int32_t)(*b)->Uid; }); #endif // prof.Add("process"); for (auto &e: m->Mail) { if (e.Uid) { #if USE_BINARY_SEARCH ImapMail *Email = f->Mail.a.BinarySearch([Uid=e.Uid](auto a) { return (int32_t)a->Uid - (int32_t)Uid; }); #else // On large folders this is sooooo slow... ImapMail *Email = f->GetMail(e.Uid); #endif // This is causing deleted email to reappear?? if (!Email && !e.Flags.ImapDeleted) { // Existing email that we don't know about? // Ok, well add new entry to track it. ImapMail *NewMail = new ImapMail(this, _FL, e.Uid); if (NewMail) MailInfToObject(DownloadMsg, &OnNew, f, NewMail, e); } if (Email) { if (Email->RemoteFlags.ImapDeleted ^ e.Flags.ImapDeleted) { if (e.Flags.ImapDeleted) Delete.Add(Email); // LAssert(!"What now?"); } else { Change.Add(Email); } Email->SetRemoteFlags(e.Flags); } } else { LgiTrace("%s:%i - No UID: seq=%i\n", _FL, e.Seq); } } if (OnNew.Length()) { f->SetDirty(); Callback->OnNew(f, OnNew, -1, true); } if (Change.Length()) { f->SetDirty(); OnChange(_FL, Change, FIELD_FLAGS); } else if (Delete.Length()) { f->SetDirty(); Callback->OnDelete(f, Delete); } if (DownloadMsg) { PostThread(DownloadMsg.Release(), false); } break; } case IMAP_LOAD_FOLDER: { // This event comes from the delayed ImapFolder::LoadMail thread... NOT the imap worker thread. if (!m->Fld.Length()) { LAssert(!"Must have 1 entry."); break; } ImapFolder *f = Root->Find(m->Fld[0].Local, NULL); if (!f) { LAssert(!"Not an IMAP folder."); break; } f->OnLoadMail(true); break; } } int Time = (int) (LCurrentTime() - Start); if (Time > 1000) { LgiTrace("%s:%i - ImapStore::OnEvent took %ims processing msg %s (len=%i)\n", _FL, Time, m && m->Type < IMAP_MSG_MAX ? ImapMsgTypeNames[m->Type] : "", m ? m->Mail.Length() : 0); } DeleteObj(m); } } //////////////////////////////////////////////////////////////////////////////////////////////// bool ValidateImapDate(LDateTime &dt) { if (dt.Year() == 2000 && dt.Hours() == 0) { LAssert(!"Invalid IMAP date."); dt.Empty(); return false; } if (dt.GetTimeZone() != 0) { LgiTrace("%s:%i - Should be a UTC date.\n", _FL); return false; } return true; } LDataStoreI *OpenImap( char *Host, int Port, char *User, char *Pass, int ConnectFlags, LDataEventsI *Callback, LCapabilityClient *Caps, MailProtocolProgress *prog[2], LStream *Log, int AccoundId, LAutoPtr store) { return new ImapStore(Host, Port, User, Pass, ConnectFlags, Callback, Caps, prog, Log, AccoundId, store); } diff --git a/src/Store3Imap/ScribeImap_Thread.cpp b/src/Store3Imap/ScribeImap_Thread.cpp --- a/src/Store3Imap/ScribeImap_Thread.cpp +++ b/src/Store3Imap/ScribeImap_Thread.cpp @@ -1,1491 +1,1492 @@ #include + #include "lgi/common/Lgi.h" -#include "lgi/common/NetTools.h" #include "lgi/common/Mail.h" +#include "lgi/common/OpenSSLSocket.h" + #include "ScribeImap.h" -#include "lgi/common/OpenSSLSocket.h" #include "ScribeUtils.h" #define DEBUG_OUTPUT_FETCHES 0 #define DEBUG_INPUT_FETCHES 0 #define MAX_LISTING_SIZE 50 #define IDLE_MAX (28 * 60 * 1000) #define ERROR_RECONNECT_TIMEOUT (10 * 1000) //static char HeaderParts[] = "UID BODY.PEEK[HEADER]"; // If you change this, check HeadersCallback as well static char BodyTag[] = "BODY.PEEK[]"; // If you change this, check DownloadCallback as well static char MailListingParts[] = "FLAGS UID RFC822.SIZE BODYSTRUCTURE BODY.PEEK[HEADER]"; static char OtherListingParts[] = "FLAGS UID"; bool ListingCallback(MailIMap *Imap, uint32_t Msg, MailIMap::StrMap &Parts, void *UserData); int FldCmp(MailImapFolder **a, MailImapFolder **b) { char *sa = (*a)->GetPath(); char *sb = (*b)->GetPath(); return _stricmp(sa, sb); } char *GetListingParts(char *Folder) { if (Folder && ( !_stricmp(Folder, "/Calendar") || !_stricmp(Folder, "/Contacts") )) return OtherListingParts; return MailListingParts; } bool FlagsCb(MailIMap *Imap, uint32_t Msg, MailIMap::StrMap &Parts, void *UserData) { ImapMailInfo *Mi = (ImapMailInfo*)UserData; char *Flags = Parts.Find("FLAGS"); if (!Flags) return false; LAssert(Mi->Uid > 0); Mi->Flags.Set(Flags); return true; } bool RecentCb(MailIMap *Imap, uint32_t Msg, MailIMap::StrMap &Parts, void *UserData) { ImapMsg *m = (ImapMsg*)UserData; char *Flags = Parts.Find("FLAGS"); // char *Body = Parts.Find("BODY.PEEK[]"); if (!Flags) return false; ImapMailInfo &i = m->Mail.New(); i.Flags.Set(Flags); return true; } struct ImapThreadPrivate : public LMutex, public LCancel { LAutoPtr Imap; ImapStore *Store; ProtocolSettingStore *SettingStore; LString Cache; LArray Msgs; ImapThread *Thread; LStream *Log; LString CurrentFolder; LCapabilityClient *Caps; char Sep[4]; LString InboxPath; LAutoPtr Listing; int64 ListingTime; int Listings; int Exists; int FetchSeq; ImapThreadPrivate( ImapThread *thread, ImapStore *s, LCapabilityClient *caps, LStream *log, ProtocolSettingStore *store) : LMutex("ImapThreadPrivate") { Sep[0] = 0; Caps = caps; SettingStore = store; FetchSeq = 1; Listings = 0; Exists = -1; Log = log; ListingTime = 0; Thread = thread; Store = s; Cache = s->GetCache(); InboxPath = "/INBOX"; } bool Cancel(bool b = true) { return LCancel::Cancel(b); } LStreamI *NewFetch(const char *Param, const char *Parts) { #if DEBUG_OUTPUT_FETCHES LFile *f = 0; char s[MAX_PATH_LEN], File[64], *c; sprintf_s(File, sizeof(File), "%04.4i %s (%s).txt", FetchSeq++, Param, Parts); while (c = strchr(File + 5, ':')) *c = '.'; LMakePath(s, sizeof(s), Cache, File); if (f = new LFile) { if (!f->Open(s, O_WRITE)) { LAssert(!"Can't open file..."); } } return f; #else return new LStringPipe(4 << 10); #endif } bool SelectFolder(const char *f, MailIMap::StrMap *Values = NULL) { if (!f) { LAssert(!"No folder."); return false; } if (!CurrentFolder || _stricmp(f, CurrentFolder)) { MailIMap::StrMap Local; Exists = -1; if (Imap->SelectFolder(f, Values ? Values : &Local)) { CurrentFolder = f; LString e = (Values ? *Values : Local).Find("Exists"); if (e) Exists = (int) e.Int(); LString s; s.Printf("Select(%s) Exists=%i\n", f, Exists); Log->Write(s.Get(), s.Length(), LSocketI::SocketMsgInfo); } else return false; } return true; } void Error(const char *file, int line, const char *s, ...) { char Buf[512]; char *Dir = strrchr((char*)file, DIR_CHAR); #ifndef WIN32 #define _snprintf snprintf #define _vsnprintf vsnprintf #endif int c = sprintf_s(Buf, sizeof(Buf), "%s:%i - ", Dir?Dir+1:file, line); va_list Arg; va_start(Arg, s); int b = vsprintf_s(Buf+c, sizeof(Buf)-c, s, Arg); va_end(Arg); c = b > 0 ? c + b : -1; if (Log && c > 0) { if (Buf[c] != '\n') Buf[c++] = '\n'; Log->Write(Buf, c, LSocketI::SocketMsgError); } } void CollectSubFolders(LHashTbl,bool> &t, char *Path) { LDirectory Dir; for (int b=Dir.First(Path); b && !IsCancelled(); b=Dir.Next()) { if (Dir.IsDir()) { char p[MAX_PATH_LEN]; Dir.Path(p, sizeof(p)); t.Add(p, true); CollectSubFolders(t, p); } } } // Do folder sync void SyncFolders() { ImapFolder *Root = CastFld(Store->GetRoot(false)); if (!Root) return; // Collect all the imap folders... LArray Folders; if (Imap->GetFolders(Folders)) { // Collect all the local folders... LHashTbl,bool> Existing; CollectSubFolders(Existing, Root->Local); LAutoPtr NewFolders; Folders.Sort(FldCmp); // For all the imap folders, check they exist locally... for (unsigned i=0; iGetSep(); Sep[1] = 0; } char p[MAX_PATH_LEN]; auto t = LString(f->Path).SplitDelimit(Sep); strcpy_s(p, sizeof(p), Root->Local); for (unsigned n=0; nPrint("SyncFolders: creating '%s'\n", f->Path); ImapFolderInfo &Inf = NewFolders->Fld.New(); Inf.Local = p; Inf.Remote = f->Path; Inf.Sep = f->GetSep(); } } } if (NewFolders && NewFolders->Fld.Length() > 0) { Store->PostStore(NewFolders.Release()); } // char *Deleted; // for (bool p=Existing.First(&Deleted); p; p=Existing.Next(&Deleted)) for (auto it : Existing) { if (Log) Log->Print("SyncFolders: deleting '%s'\n", it.key); ImapMsg *del_folder = new ImapMsg(IMAP_ON_DEL, _FL); if (del_folder) { ImapFolderInfo &Inf = del_folder->Fld.New(); Inf.Local = it.key; Store->PostStore(del_folder); } } Folders.DeleteObjects(); } else LgiTrace("%s:%i - GetFolders failed.\n", _FL); } bool DownloadIds(char *Parts, bool Uid, LString::Array &Ids) { auto Lst = LString(",").Join(Ids); LAutoPtr Raw(NewFetch("All", Parts)); LAutoPtr m(new ImapMsg(IMAP_ON_NEW, _FL)); if (!m) return false; if (!Imap->Fetch(Uid, Lst, Parts, ListingCallback, m, Raw)) return false; m->New = true; m->Parent = CurrentFolder.Get(); Thread->PostStore(m.Release()); return true; } }; ImapThread::ImapThread(ImapStore *s, LCapabilityClient *caps, LStream *log, ProtocolSettingStore *store) : LThread("ImapThread.Thread"), LMutex("ImapThread.Mutex") { if ((d = new ImapThreadPrivate(this, s, caps, log, store))) { Run(); } } ImapThread::~ImapThread() { d->Cancel(true); auto Start = LCurrentTime(); while (!IsExited()) { LSleep(10); if (LCurrentTime() - Start > 2000) { LAssert(!"Why is it taking so long?"); } } DeleteObj(d); } void ImapThread::PostThread(ImapMsg *m, bool UiPriority) { if (Lock(_FL)) { if (d->Store->ItemProgress) { if (d->Store->ItemProgress->Range == 0) d->Store->ItemProgress->Start = LCurrentTime(); if (d->Store->DataProgress) d->Store->DataProgress->Start = LCurrentTime(); d->Store->ItemProgress->Range++; } if (UiPriority) d->Msgs.AddAt(0, m); else d->Msgs.Add(m); Unlock(); } } void ImapThread::PostStore(ImapMsg *m) { if (m) d->Store->PostStore(m); } /* bool IdleCallback(MailIMap *Imap, char *Msg, GHashTbl &Parts, void *UserData) { ImapMsg *m = (ImapMsg*) UserData; if (!m) return false; char *Flags = Parts.Find("FLAGS"); char *Uid = Parts.Find("UID"); if (Flags && Uid) { ImapMailInfo &u = m->Mail.New(); u.Uid.Reset(NewStr(Uid)); u.Flags.Set(Flags); } else return false; return true; } */ bool ListingCallback(MailIMap *Imap, uint32_t Msg, MailIMap::StrMap &Parts, void *UserData) { ImapMsg *m = (ImapMsg*) UserData; if (!m) { LAssert(!"No user data."); return false; } char *Flags = Parts.Find("FLAGS"); char *Uid = Parts.Find("UID"); char *Rfc822Size = Parts.Find("RFC822.SIZE"); char *BodyStructure = Parts.Find("BODYSTRUCTURE"); char *Header = Parts.Find("BODY[HEADER]"); #if DEBUG_INPUT_FETCHES const char *k; for (char *v = Parts.First(&k); v; v = Parts.Next(&k)) { LgiTrace("%s\n", k); } #endif if (!Uid) { LAssert(!"No uid?"); return true; // Lets get all we can, even if this is borked. } uint32_t uid = atoi(Uid); #ifdef _DEBUG for (unsigned n=0; nMail.Length(); n++) { if (m->Mail[n].Uid == uid) { LAssert(!"Duplicate UID."); return true; } } #endif if (m->Fld.Length() > 0 && uid <= m->Fld[0].LastUid) { // WTF server? (dovecot brokenness) } else { ImapMailInfo &i = m->Mail.New(); i.Seq = Msg; i.Flags.Set(Flags); i.Uid = atoi(Uid); i.Structure = BodyStructure; if (Header) { i.Headers = Header; LAutoString Date(InetGetHeaderField(Header, "Date", -1)); if (Date) i.Date = Date; #if DEBUG_INPUT_FETCHES else LgiTrace("%s:%i - No date field? (Uid=%i)\n", _FL, i.Uid); #endif } #if DEBUG_INPUT_FETCHES else LgiTrace("%s:%i - No header?\n", _FL); #endif if (Rfc822Size) i.Size = Atoi(Rfc822Size); } return true; } void ImapThread::Log(LSocketI::SocketMsgType Type, const char *Fmt, ...) { LString s; va_list arg; va_start(arg, Fmt); s.Printf(arg, Fmt); va_end(arg); d->Log->Write(s.Get(), s.Length(), Type); } ImapMsg *ImapThread::GetListing() { if (!d->Listing) { d->Listing.Reset(new ImapMsg(IMAP_FOLDER_LISTING, _FL)); d->ListingTime = LCurrentTime(); } return d->Listing; } void ImapThread::FlushListing(bool Force) { if (d->Listing) { int64 Now = LCurrentTime(); if (Force || !d->ListingTime || Now - d->ListingTime > TIMEOUT_LISTING_CHUNK || d->Listing->Mail.Length() >= MAX_LISTING_SIZE) { d->Listings += (int)d->Listing->Mail.Length(); // LgiTrace("Flushing %i listings (%i)\n", d->Listing->Ids.Length(), d->Listings); d->Listing->Last = Force; PostStore(d->Listing.Release()); } } } bool HeadersCallback(MailIMap *Imap, char *Msg, LHashTbl, char*> &Parts, void *UserData) { ImapMsg *m = (ImapMsg*) UserData; if (m) { char *Uid = Parts.Find("UID"); char *Headers = Parts.Find("BODY[HEADER]"); if (Headers && Uid) { ImapMailInfo &i = m->Mail.New(); i.Uid = atoi(Uid); i.Headers = Headers; return true; } LgiTrace("%s:%i - Missing fields: %p,%p?", _FL, Headers, Uid); } return false; } struct DownloadInfo { uint64_t Uid; LString Parent; LVariant Local; ImapThread *Thread; }; bool DownloadCallback(MailIMap *Imap, uint32_t Msg, MailIMap::StrMap &Parts, void *UserData) { DownloadInfo *Inf = (DownloadInfo*) UserData; char *sUid = Parts.Find("UID"); char *Body = Parts.Find("BODY[]"); if (sUid && Body) { int Uid = atoi(sUid); // Do a UID check on the email... if (!Inf->Uid || Uid != Inf->Uid) { // This is probably caused by a FETCH result parsing error. // LAssert(!"Uid mismatch."); LgiTrace("%s:%i - Uid mismatch: %i %i\n", _FL, Uid, Inf->Uid); return false; } else { // Write it out (loop is because windows is shit) LFile f; for (int i=0; !f.Open(Inf->Local.Str(), O_WRITE) && i<100; i++) LSleep(20); if (f) { size_t BodyLen = strlen(Body); if (BodyLen < 8) { // Yargh, there be trouble brewin... LAssert(!"No body? Really?"); } else { f.SetSize(0); f.Write(Body, (int)BodyLen); f.Close(); ImapMsg *Msg = new ImapMsg(IMAP_DOWNLOAD, _FL); if (Msg) { ImapMailInfo &i = Msg->Mail.New(); i.Uid = Uid; Msg->Parent = Inf->Parent; Inf->Thread->PostStore(Msg); } } } else { LAssert(!"Can't open IMAP cache file?"); LgiTrace("%s:%i - Failed to open '%s' for writing.\n", _FL, Inf->Local.Str()); return false; } } } else LgiTrace("%s:%i - Missing parts: %p %p\n", _FL, sUid, Body); return true; } template class ImapLogSocket : public T { LAutoPtr File; MailProtocolProgress *Prog; public: ImapLogSocket(char *file, LCapabilityClient *caps, LStreamI *logger, MailProtocolProgress *prog) : T(logger, caps) { Prog = prog; if (file && File.Reset(new LFile)) { if (File->Open(file, O_WRITE)) File->SetPos(File->GetSize()); else File.Reset(); } } ~ImapLogSocket() { if (File) File->ChangeThread(); } ssize_t Read(void *ptr, ssize_t size, int flags) { memset(ptr, 0, size); ssize_t r = T::Read(ptr, size, flags); if (r > 0) { if (Prog) Prog->Value += r; if (File) File->Write(ptr, r); } return r; } ssize_t Write(const void *ptr, ssize_t size, int flags) { if (Prog) Prog->Value += size; if (File) File->Write(ptr, size); return T::Write(ptr, size, flags); } }; bool UidCb(MailIMap *Imap, uint32_t Msg, MailIMap::StrMap &Parts, void *UserData) { ImapMsg *m = (ImapMsg*)UserData; auto sUid = Parts.Find("UID"); if (sUid && Msg) { bool Found = false; for (auto &i: m->Mail) { if (i.Seq == Msg) { i.Uid = atoi(sUid); LgiTrace("%s:%i - Msg=%i has Uid=%i\n", _FL, Msg, i.Uid); Found = true; break; } } if (!Found) LgiTrace("%s:%i - No Msg='%s'\n", _FL, Msg); } else LgiTrace("%s:%i - Needs Uid and Msg\n", _FL); return true; } int ImapThread::Main() { bool Online = false, InIdle = false; int IdleCount = 0; uint64 IdleStart = 0; uint64 ErrorStart = 0; while (!d->IsCancelled()) { if (d->Imap && !d->Imap->IsOnline()) { // Lost connection... delete and start again if (d->Lock(_FL)) { d->Imap.Reset(); d->Unlock(); } } if ( !d->Imap && ( !ErrorStart || (LCurrentTime() - ErrorStart > ERROR_RECONNECT_TIMEOUT) ) ) { // Setup a new imap protocol object if (d->Lock(_FL)) { d->Imap.Reset(new MailIMap); d->Unlock(); } if (d->Imap) { LUri u(d->Store->Host.Str()); if (u.Port <= 0 && d->Store->Port > 0) { u.Port = d->Store->Port; } d->Imap->Logger = d->Store->GetLogger(); char LogFileName[MAX_PATH_LEN] = "", Part[32]; LVariant LogFmt; if (d->SettingStore->GetOptions()->GetValue(OPT_LogFormat, LogFmt) && LogFmt.CastInt32() > 0) { sprintf_s(Part, sizeof(Part), "../%i_log.txt", d->Store->AccountId); LAssert(d->Cache != NULL); LMakePath(LogFileName, sizeof(LogFileName), d->Cache, Part); } // Connect to the server LSocketI *Sock = 0; bool SslDirect = (d->Store->ConnectFlags & MAIL_SSL) != 0; bool StartTls = (d->Store->ConnectFlags & MAIL_USE_STARTTLS) != 0; if (SslDirect || StartTls) { ImapLogSocket *s = new ImapLogSocket( LogFileName[0] ? LogFileName : NULL, d->Caps, d->Store->GetLogger(), d->Store->DataProgress); Sock = s; if (s) s->SetSslOnConnect(SslDirect); } else { Sock = new ImapLogSocket( LogFileName[0] ? LogFileName : NULL, d->Caps, d->Store->GetLogger(), d->Store->DataProgress); } LOAuth2::Params p = GetOAuth2Params(u.sHost, MAGIC_MAIL); if (p.Provider != LOAuth2::Params::None) { d->Imap->SetOAuthParams(p); d->Imap->SetCancel(d); d->Imap->SetParentWindow(dynamic_cast(d->Store->Callback)); } Sock->SetCancel(d); if (d->Imap->Open( Sock, u.sHost, u.Port, d->Store->User.Str(), d->Store->Pass.Str(), d->SettingStore, d->Store->ConnectFlags)) { ErrorStart = 0; d->CurrentFolder.Empty(); if (d->Log) d->Log->Print("Connected to %s:%i\n", u.sHost.Get(), u.Port); // Sync our folders d->SyncFolders(); } else { // Failed to connect... start again if (d->Lock(_FL)) { d->Imap.Reset(); d->Unlock(); } if (u.Port == 0) { if (d->Store->ConnectFlags & MAIL_SSL) u.Port = IMAP_SSL_PORT; else u.Port = IMAP_PORT; } d->Error(_FL, "IMAP connection to %s:%i failed.", u.sHost.Get(), u.Port); if (ErrorStart == 0) PostStore(new ImapMsg(IMAP_ERROR, _FL)); ErrorStart = LCurrentTime(); } } } bool CurrentlyOnline = d->Imap ? d->Imap->IsOnline() : false; if (CurrentlyOnline ^ Online) { PostStore(new ImapMsg(CurrentlyOnline ? IMAP_ONLINE : IMAP_OFFLINE, _FL)); Online = CurrentlyOnline; } if (!d->Imap) { LSleep(50); continue; } LAutoPtr m; if (Lock(_FL)) { if (d->Msgs.Length()) { m.Reset(d->Msgs[0]); d->Msgs.DeleteAt(0, true); uint64 Now = LCurrentTime(); if (d->Store->DataProgress && (Now - d->Store->DataProgress->Start) > 10000) { d->Store->DataProgress->Start = Now; d->Store->DataProgress->Value = 0; } } else { if (d->Store->ItemProgress) d->Store->ItemProgress->Empty(); if (d->Store->DataProgress) d->Store->DataProgress->Empty(); } Unlock(); } if (m) { // This block skips the idle stop code if the folder select is the current folder, // which in turn is a noop anyway. if (m->Type == IMAP_SELECT_FOLDER && m->Fld.Length() == 1 && m->Fld[0].Remote && d->CurrentFolder && !strcmp(m->Fld[0].Remote, d->CurrentFolder)) { continue; } // If we're currently in idle state, we need to stop that before we issue another command if (InIdle) { IdleCount = 0; d->Imap->FinishIdle(); InIdle = false; IdleStart = 0; } // Process the command from the GUI thread. switch (m->Type) { default: break; case IMAP_SELECT_FOLDER: { if (m->Fld.Length() == 1) { ImapFolderInfo &Inf = m->Fld[0]; MailIMap::StrMap Values; if (d->SelectFolder(Inf.Remote, &Values)) { LString Recent = Values.Find("Recent"); if (Recent && Recent.Int() > 0) { LString::Array Uids; if (d->Imap->Search(true, Uids, "recent")) { d->DownloadIds(GetListingParts(Inf.Remote), true, Uids); } } } } break; } case IMAP_FOLDER_LISTING: { // This gets us the UID/Flags and sequence numbers. for (unsigned i=0; iFld.Length(); i++) { ImapFolderInfo &Inf = m->Fld[i]; LAssert(Inf.Remote); if (d->SelectFolder(Inf.Remote)) { char UidRange[32]; char *ListingParts = GetListingParts(Inf.Remote); LAutoPtr Raw(d->NewFetch("All", ListingParts)); LAutoPtr Resp(new ImapMsg(IMAP_FOLDER_LISTING, _FL)); ImapFolderInfo &r = Resp->Fld.New(); r.LastUid = Inf.LastUid; sprintf_s(UidRange, sizeof(UidRange), "%i:*", Inf.LastUid > 0 ? Inf.LastUid + 1 : 1); if (d->Imap->Fetch( true, UidRange, ListingParts, ListingCallback, Resp, Raw)) { r.Local = Inf.Local; r.Remote = Inf.Remote; Resp->Last = 1; PostStore(Resp.Release()); } else d->Error(_FL, "Fetch failed."); } } break; } case IMAP_DOWNLOAD: { LAssert(m->Parent); if (!d->SelectFolder(m->Parent)) break; for (auto &mi: m->Mail) { LAutoPtr Inf(new DownloadInfo); if (Inf) { Inf->Uid = mi.Uid; Inf->Parent = m->Parent; Inf->Local = m->Mail[0].Local; // m->Local.Str(); Inf->Thread = this; MailProtocolProgress *p = m->Mail.Length() == 1 ? d->Store->DataProgress : NULL; if (p && mi.Size > 0) { if (p) p->StartTransfer((int)mi.Size); if (mi.Size > (100 << 10) && d->Log) d->Log->Print("Fetching %s...\n", LFormatSize(mi.Size).Get()); } LString s; s.Printf("%i", mi.Uid); LError Err; int Status = d->Imap->Fetch(true, s, BodyTag, DownloadCallback, Inf, NULL, mi.Size, &Err); if (!Status) { // Ok the fetch failed because the object doesn't exist anymore? // We should delete it from the store right? Hmmmm... ImapMsg *Msg = new ImapMsg(IMAP_ON_DEL, _FL); if (Msg) { Msg->Error = Err; ImapMailInfo &i = Msg->Mail.New(); i.Uid = mi.Uid; Msg->Parent = Inf->Parent; PostStore(Msg); } } if (p) p->StartTransfer(0); } } break; } case IMAP_CREATE_FOLDER: { ImapMsg *Failed = NULL; for (unsigned i=0; iFld.Length(); i++) { ImapFolderInfo &Inf = m->Fld[i]; if (Inf.Remote && Inf.Local) { MailImapFolder t; t.SetPath(Inf.Remote); if (!d->Imap->CreateFolder(&t)) { // Create a failed response for the store... if (!Failed) Failed = new ImapMsg(IMAP_ON_DEL, _FL); if (Failed) { ImapFolderInfo &k = Failed->Fld.New(); k.Local = Inf.Local; k.Remote = Inf.Remote; } // Remove the failed entry from the success msg m->Fld.DeleteAt(i--); } } } if (m->Fld.Length() > 0) { m->SetType(m->Type, _FL); PostStore(m.Release()); } if (Failed) PostStore(Failed); break; } case IMAP_DELETE_FOLDER: { for (unsigned i=0; iFld.Length(); i++) { ImapFolderInfo &Inf = m->Fld[i]; if (Inf.Remote && Inf.Local) { if (d->Imap->DeleteFolder(Inf.Remote)) { ImapMsg *Fail = new ImapMsg(IMAP_ON_DEL, _FL); if (Fail) { ImapFolderInfo &k = Fail->Fld.New(); k.Local = Inf.Local; k.Remote = Inf.Remote; d->Store->PostStore(Fail); } } } } break; } case IMAP_MOVE_EMAIL: { if (!m->NewRemote || !m->Parent) { d->Error(_FL, "Argument missing."); break; } // The store should've broken the deletes up into // blocks for us. LAssert(m->Mail.Length() <= IMAP_BLOCK_SIZE); if (!d->SelectFolder(m->Parent)) { d->Error(_FL, "Imap select failed."); break; } LArray Lst; for (unsigned i=0; iMail.Length(); i++) Lst.Add(m->Mail[i].Uid); if (!d->Imap->CopyByUid(Lst, m->NewRemote)) { d->Error(_FL, "Imap copy failed."); break; } // Delete the source... if (!d->Imap->SetFlagsByUid(Lst, "\\Deleted")) { d->Error(_FL, "Imap DeleteByUid() failed."); break; } m->SetType(IMAP_ON_DEL, _FL); LString Remote = m->NewRemote; d->Store->PostStore(m.Release()); MailIMap::StrMap NewFolderValues; auto OldFolder = d->CurrentFolder; if (d->SelectFolder(Remote, &NewFolderValues) && m.Reset(new ImapMsg(IMAP_ON_NEW, _FL))) { m->Parent = Remote; // Add the new messages to 'm' here // Not sure of the UIDs so search them LString::Array Uids; if (d->Imap->Search(true, Uids, "RECENT")) { d->DownloadIds(GetListingParts(d->CurrentFolder), true, Uids); } else { // Reselect the previous folder... d->SelectFolder(OldFolder); } } break; } case IMAP_DELETE: { if (d->SelectFolder(m->Parent)) { LArray Ids; // The store should've broken the deletes up into // blocks for us. LAssert(m->Mail.Length() <= IMAP_BLOCK_SIZE); for (unsigned i=0; iMail.Length(); i++) Ids.Add(m->Mail[i].Uid); LgiTrace("IMAP_DELETE....\n"); if (d->Imap->SetFlagsByUid(Ids, "\\Deleted")) { LgiTrace("Sending IMAP_ON_DEL....\n"); m->SetType(IMAP_ON_DEL, _FL); PostStore(m.Release()); } else { LgiTrace("SetFlagsByUid failed....\n"); } } break; } case IMAP_UNDELETE: { if (d->SelectFolder(m->Parent)) { LArray Ids; for (unsigned i=0; iMail.Length(); i++) { Ids.Add(m->Mail[i].Uid); } if (d->Imap->SetFlagsByUid(Ids, "\\seen")) { // m->SetType(IMAP_ON_DEL, _FL); // PostStore(m.Release()); } } break; } case IMAP_SET_FLAGS: { // Set the flags on message(s) if (m->Parent) d->SelectFolder(m->Parent); LHashTbl, LArray*> Map(0, NULL); // Group all the messages into similar flags groups for (auto &mail: m->Mail) { auto map = Map.Find(mail.Flags.All); if (!map) Map.Add(mail.Flags.All, map = new LArray); map->Add(mail.Uid); } for (auto &p: Map) { auto flags = p.key; auto arr = p.value; ImapMailFlags f; f.All = flags; f.ImapRecent = 0; auto Flags = f.Get(); d->Imap->SetFlagsByUid(*arr, Flags); LAutoPtr resp(new ImapMsg(IMAP_SET_FLAGS, _FL)); if (resp) { resp->Parent = m->Parent.Get(); for (auto i: *arr) { auto &n = resp->Mail.New(); n.Uid = i; n.Flags = f; } d->Store->PostStore(resp.Release()); } } Map.DeleteObjects(); break; } case IMAP_APPEND: { for (unsigned i=0; iMail.Length(); i++) { ImapMailInfo &mi = m->Mail[i]; LAutoString Rfc822(LReadTextFile(mi.Local)); LString NewUid; if (d->Imap->Append(m->Parent, &mi.Flags, Rfc822, NewUid)) { if (NewUid) m->Mail[i].Uid = atoi(NewUid); else { // Do a specific search to find the UID.... // (Although this is not tested and doesn't work on at least exchange...) char Key[256]; LArray Uids; LAutoString MessageId(InetGetHeaderField(Rfc822, "Message-Id")); MessageId.Reset(TrimStr(MessageId, "<>")); sprintf_s(Key, sizeof(Key), "HEADER Message-Id \"%s\"", MessageId.Get()); if (d->Imap->Search(true, Uids, Key)) { if (Uids.Length() > 0) m->Mail[i].Uid = (uint32_t)Uids[0].Int(); else LAssert(!"No uid?"); } // else LAssert(!"No search results?"); } } } m->SetType(m->Type, _FL); PostStore(m.Release()); break; } case IMAP_EXPUNGE_FOLDER: { for (unsigned i=0; iFld.Length(); i++) { if (d->SelectFolder(m->Fld[i].Remote)) { if (!d->Imap->ExpungeFolder()) { m->Fld.DeleteAt(i); i--; } } } m->SetType(m->Type, _FL); PostStore(m.Release()); break; } case IMAP_RENAME_FOLDER: { char *Old = m->Parent; if (!Old) break; if (*Old == d->Sep[0]) Old++; char *New = m->NewRemote; if (!New) break; if (*New == d->Sep[0]) New++; bool r = d->Imap->RenameFolder(Old, New); if (r) { m->SetType(m->Type, _FL); PostStore(m.Release()); } break; } } } else { if (InIdle) { LString::Array Resp; if (d->Imap->OnIdle(200, Resp)) { LAutoPtr FlagsUpdate; LAutoPtr ExpungeUpdate; for (unsigned i=0; i Ranges; d->Imap->ParseImapResponse(u, u.Length(), Ranges, 2); if (Ranges.Length() < 2) continue; #define RngStr(idx, name) LString name; \ { auto &r = Ranges[idx]; name.Set(u.Get() + r.Start, r.Len()); } RngStr(0, Id); RngStr(1, Cmd); if (Cmd.Equals("RECENT")) { d->Imap->FinishIdle(); LArray Numbers; if (d->Imap->Search(true, Numbers, "UNDELETED UNSEEN")) { LStringPipe n(64); for (unsigned i=0; i New(new ImapMsg(IMAP_ON_NEW, _FL)); if (d->Imap->Fetch(true, Range, GetListingParts(d->CurrentFolder), ListingCallback, New)) { New->Parent = d->CurrentFolder.Get(); New->New = true; PostStore(New.Release()); } else d->Error(_FL, "IDLE FETCH failed\n"); } if ((InIdle = d->Imap->StartIdle())) IdleStart = LCurrentTime(); } else if (Cmd.Equals("EXISTS")) { int NewExists = (int)Id.Int(); Log(LSocketI::SocketMsgInfo, "Got EXISTS: %i->%i\n", d->Exists, NewExists); if (d->Exists >= 0 && NewExists > d->Exists) { d->Imap->FinishIdle(); char Range[32]; if (NewExists == d->Exists + 1) sprintf_s(Range, sizeof(Range), "%i", NewExists); else sprintf_s(Range, sizeof(Range), "%i:%i", d->Exists + 1, NewExists); LAutoPtr Update(new ImapMsg(IMAP_SET_FLAGS, _FL)); int b = d->Imap->Fetch(false, Range, GetListingParts(d->CurrentFolder), ListingCallback, Update); if (b) { Update->Parent = d->CurrentFolder.Get(); PostStore(Update.Release()); } else d->Error(_FL, "IDLE FETCH failed\n"); if ((InIdle = d->Imap->StartIdle())) IdleStart = LCurrentTime(); } else Log(LSocketI::SocketMsgInfo, "Exists no action: %i, %i\n", d->Exists, NewExists); d->Exists = NewExists; } else if (Cmd.Equals("FETCH")) { uint32_t Uid = 0; for (int i = 2; i < Ranges.Length() - 1; i += 2) { RngStr(i, Fld); RngStr(i+1, Val); if (Fld.Equals("UID")) { Uid = (uint32_t)Val.Int(); } else if (Fld.Equals("FLAGS")) { if (FlagsUpdate || FlagsUpdate.Reset(new ImapMsg(IMAP_SET_FLAGS, _FL))) { if (!FlagsUpdate->Parent) FlagsUpdate->Parent = d->CurrentFolder.Get(); auto &m = FlagsUpdate->Mail.New(); m.Seq = (uint32_t)Id.Int(); m.Uid = Uid; m.Flags.Set(Val); } } else { LgiTrace("%s:%i - Unknown FETCH field: %s\n", _FL, Fld.Get()); } } } else if (Cmd.Equals("EXPUNGE")) { if (ExpungeUpdate || ExpungeUpdate.Reset(new ImapMsg(IMAP_EXPUNGE_FOLDER, _FL))) { if (!ExpungeUpdate->Parent) ExpungeUpdate->Parent = d->CurrentFolder.Get(); ImapMailInfo &Mi = ExpungeUpdate->Mail.New(); Mi.Seq = (uint32_t)Id.Int(); } } else if (Id.Equals("OK")) { // Fluff... } else { d->Error(_FL, "Unexpected IDLE message: %s", Cmd.Get()); } } if (FlagsUpdate) { LString::Array Seqs; Seqs.SetFixedLength(false); for (auto &m: FlagsUpdate->Mail) { if (!m.Uid) Seqs.New().Printf("%i", m.Seq); } if (Seqs.Length()) { LString Val = LString(",").Join(Seqs); // At this point we have some message sequence numbers. But we need the UIDs. d->Imap->FinishIdle(); d->Imap->Fetch(false, Val, "UID", UidCb, FlagsUpdate.Get()); if ((InIdle = d->Imap->StartIdle())) IdleStart = LCurrentTime(); } // Now send the message PostStore(FlagsUpdate.Release()); } if (ExpungeUpdate) { PostStore(ExpungeUpdate.Release()); } } if (LCurrentTime() > IdleStart + IDLE_MAX) { d->Imap->FinishIdle(); if ((InIdle = d->Imap->StartIdle())) IdleStart = LCurrentTime(); } } else if (IdleCount > 5000 / 50) { if (!d->CurrentFolder.Equals(d->InboxPath)) d->SelectFolder(d->InboxPath); if ((InIdle = d->Imap->StartIdle())) { IdleStart = LCurrentTime(); } else { // This can happen when the connection dies... so closing // it will allow proper cleanup and re-connection. if (d->Lock(_FL)) { d->Imap->Close(); d->Unlock(); } } } else if (IdleCount >= 0) { IdleCount++; LSleep(50); } } if (d->Store->ItemProgress) d->Store->ItemProgress->Value++; } if (d->Store->ItemProgress) d->Store->ItemProgress->Empty(); if (d->Store->DataProgress) d->Store->DataProgress->Empty(); return 0; } diff --git a/src/Store3Mail3/Mail3Mail.cpp b/src/Store3Mail3/Mail3Mail.cpp --- a/src/Store3Mail3/Mail3Mail.cpp +++ b/src/Store3Mail3/Mail3Mail.cpp @@ -1,1623 +1,1622 @@ #include "Mail3.h" -#include "lgi/common/NetTools.h" #include "lgi/common/Mail.h" #include "lgi/common/Store3MimeTree.h" #include "lgi/common/StreamConcat.h" #include "lgi/common/TextConvert.h" #include "ScribeUtils.h" LMail3Def TblMail[] = { {"Id", "INTEGER PRIMARY KEY AUTOINCREMENT"}, {"ParentId", "INTEGER"}, // Integers {"Priority", "INTEGER"}, {"Flags", "INTEGER"}, {"AccountId", "INTEGER"}, {"MarkColour", "INTEGER"}, // Strings {"Subject", "TEXT"}, {"ToAddr", "TEXT"}, {"FromAddr", "TEXT"}, {"Reply", "TEXT"}, {"Label", "TEXT"}, {"InternetHeader", "TEXT"}, {"MessageID", "TEXT"}, {"Ref", "TEXT"}, {"FwdMsgId", "TEXT"}, {"BounceMsgId", "TEXT"}, {"ServerUid", "TEXT"}, // Date/times {"DateReceived", "TEXT"}, {"DateSent", "TEXT"}, // Other meta data {"Size", "INTEGER"}, {0, 0} }; LMail3Def TblMailSegs[] = { {"Id", "INTEGER PRIMARY KEY AUTOINCREMENT"}, {"MailId", "INTEGER"}, {"ParentId", "INTEGER"}, {"Headers", "TEXT"}, {"Data", "BLOB"}, {0, 0}, }; bool Mail3_InsertSeg(LMail3Store::LInsert &Ins, LMime *m, int64 ParentId, int64 ParentSeg, int64 &MailSize) { Ins.SetInt64(1, ParentId); Ins.SetInt64(2, ParentSeg); Ins.SetStr(3, m->GetHeaders()); LStreamI *MimeData = m->GetData(); int64 Sz = MimeData ? MimeData->GetSize() : 0; if (Sz > 0) MailSize += Sz; Ins.SetStream(4, "Data", MimeData); if (!Ins.Exec()) return false; ParentSeg = Ins.LastInsertId(); Ins.Reset(); for (int i=0; iLength(); i++) { LMime *c = (*m)[i]; if (!Mail3_InsertSeg(Ins, c, ParentId, ParentSeg, MailSize)) return false; } return true; } bool Mail3_CopySegs(LMail3Mail *m, LMail3Attachment *parent, LDataI *in) { const char *Mt = in->GetStr(FIELD_MIME_TYPE); if (!Mt) { return false; } LMail3Attachment *out = new LMail3Attachment(m->Store); if (!out) return false; out->CopyProps(*in); if (parent) out->AttachTo(parent); else out->AttachTo(m); LDataIt It = in->GetList(FIELD_MIME_SEG); for (LDataPropI *c = It->First(); c; c = It->Next()) { LDataI *child = dynamic_cast(c); if (child && !Mail3_CopySegs(m, out, child)) return false; } return true; } LMail3Mail::LMail3Mail(LMail3Store *store) : LMail3Thing(store), From(store), Reply(store) { To.State = Store3Loaded; } LMail3Mail::~LMail3Mail() { To.DeleteObjects(); DeleteObj(Seg); } #define DEBUG_COPY_PROPS 0 Store3CopyImpl(LMail3Mail) { #if DEBUG_COPY_PROPS LProfile Prof("Store3CopyProps"); #endif Priority = (int)p.GetInt(FIELD_PRIORITY); Flags = (int)p.GetInt(FIELD_FLAGS); #if DEBUG_COPY_PROPS Prof.Add(_FL); #endif SetStr(FIELD_INTERNET_HEADER, p.GetStr(FIELD_INTERNET_HEADER)); MessageID = p.GetStr(FIELD_MESSAGE_ID); Subject = p.GetStr(FIELD_SUBJECT); MarkColour = (int)p.GetInt(FIELD_COLOUR); AccountId = (int)p.GetInt(FIELD_ACCOUNT_ID); SetStr(FIELD_LABEL, p.GetStr(FIELD_LABEL)); #if DEBUG_COPY_PROPS Prof.Add(_FL); #endif LDataPropI *i = p.GetObj(FIELD_FROM); if (i) From.CopyProps(*i); i = p.GetObj(FIELD_REPLY); if (i) Reply.CopyProps(*i); #if DEBUG_COPY_PROPS Prof.Add(_FL); #endif const LDateTime *d = p.GetDate(FIELD_DATE_RECEIVED); if (d) DateReceived = *d; d = p.GetDate(FIELD_DATE_SENT); if (d) DateSent = *d; #if DEBUG_COPY_PROPS Prof.Add(_FL); #endif To.DeleteObjects(); LDataIt pTo = p.GetList(FIELD_TO); for (unsigned n=0; nLength(); n++) { To.Insert(new Store3Addr(GetStore(), (*pTo)[n]), -1, true); } To.State = Store3Loaded; #if DEBUG_COPY_PROPS Prof.Add(_FL); #endif LDataI *Root = dynamic_cast(p.GetObj(FIELD_MIME_SEG)); if (Root) { // Must commit ourselves now and get an Id if needed #if DEBUG_COPY_PROPS Prof.Add(_FL); #endif if (Id > 0 || Write(MAIL3_TBL_MAIL, true)) { // Clear existing segment if present... LAssert(!Seg || Seg->GetId() < 0); DeleteObj(Seg); #if DEBUG_COPY_PROPS Prof.Add(_FL); #endif // Save all the segments out Mail3_CopySegs(this, NULL, Root); #if DEBUG_COPY_PROPS Prof.Add(_FL); #endif OnSave(); } } return true; } void LMail3Mail::SetStore(LMail3Store *s) { if (Id < 0) { LMail3Thing::SetStore(s); From.SetStore(s); Reply.SetStore(s); if (Seg) Seg->SetMail(this); } else LAssert(!"Object is already commited to another store."); } bool SafeAddrTokenize(const char *In, List &Out, bool Debug = false) { if (!In) return false; // Find all the characters of interest... LArray a; for (const char *c = In; *c; c++) { if (strchr("\':,<@>", *c)) a.Add(c); } // Now do pattern matching. We may encounter weird numbers of single // quotes due to recipient names having un-escaped single quotes. const char *Last = In; LAutoString Str; for (ssize_t i=0; i<(ssize_t)a.Length(); i++) { // Check for '<' '@', '>' pattern if (i < (ssize_t)a.Length() - 4 && *a[i] == '<' && *a[i+1] == '@' && *a[i+2] == '>' && *a[i+3] == ':' && *a[i+4] == ',') { const char *e = a[i+4]; Str.Reset(NewStr(Last, e-Last)); Last = e + 1; } else if (i < (ssize_t)a.Length() - 3 && *a[i] == '<' && *a[i+1] == '@' && *a[i+2] == '>' && *a[i+3] == ':') { const char *e = a[i+3]; while (*e) e++; Str.Reset(NewStr(Last, e-Last)); Last = e; } else if (i < (ssize_t)a.Length() - 2 && *a[i] == '<' && *a[i+1] == '>' && *a[i+2] == ':') { // This case handles a group name without a '@' in it... const char *e = a[i+2]; while (*e) e++; Str.Reset(NewStr(Last, e-Last)); Last = e; } else break; if (Str) { Out.Insert(Str.Release()); } } return true; } #if 1 #define DEBUG_SERIALIZE(...) if (Debug) LgiTrace(__VA_ARGS__) #else #define DEBUG_SERIALIZE(...) #endif bool LMail3Mail::Serialize(LMail3Store::LStatement &s, bool Write) { static LVariant vTo, vFrom, vReply; bool Debug = false; // Save the objects to strings if (Write) { if (MailSize < 0 && Seg) { MailSize = (uint32_t) (Seg->Size() + Seg->SizeChildren()); } if (!From.GetValue("Text", vFrom)) vFrom.Empty(); if (!Reply.GetValue("Text", vReply)) vReply.Empty(); LStringPipe p; int Done = 0; for (unsigned i=0; iGetValue("Text", v) && v.Str()) { const char *Comma = Done ? ", " : ""; DEBUG_SERIALIZE("ToText='%s'\n", v.Str()); p.Print("%s%s:%i", Comma, v.Str(), a->CC); Done++; } } vTo.OwnStr(p.NewStr()); DEBUG_SERIALIZE("vTo='%s'\n", vTo.Str()); } int i = 0; LVariant NullInternetHeader; // The internet header is now stored // In the first segment ONLY. But it // wasn't worth removing the field in // the DB. SERIALIZE_INT64(Id, i++); SERIALIZE_INT64(ParentId, i++); SERIALIZE_INT(Priority, i++); SERIALIZE_INT(Flags, i++); SERIALIZE_INT(AccountId, i++); SERIALIZE_INT(MarkColour, i++); SERIALIZE_STR(Subject, i++); SERIALIZE_STR(vTo, i++); SERIALIZE_STR(vFrom, i++); SERIALIZE_STR(vReply, i++); SERIALIZE_STR(Label, i++); SERIALIZE_STR(NullInternetHeader, i++); SERIALIZE_STR(MessageID, i++); SERIALIZE_STR(References, i++); SERIALIZE_STR(FwdMsgId, i++); SERIALIZE_STR(BounceMsgId, i++); SERIALIZE_STR(ServerUid, i++); SERIALIZE_DATE(DateReceived, i++); SERIALIZE_DATE(DateSent, i++); SERIALIZE_INT64(MailSize, i++); // Load the objects from the strings if (!Write) { From.SetValue("Text", vFrom); Reply.SetValue("Text", vReply); To.DeleteObjects(); List Addr; char *ToStr = vTo.Str(); //DEBUG_SERIALIZE("ReadTo='%s'\n", ToStr); int Colons = 0; int Commas = 0; for (char *c = ToStr; c && *c; c++) { if (*c == ':') Colons++; else if (*c == ',') Commas++; } if (Commas == 1 && Colons > 1) { // Broken data char *c = ToStr; while (*c) { while (*c && (strchr(WhiteSpace, *c) || *c == ',')) c++; char *e = strchr(c, '>'); if (!e) break; e++; if (*e == ':') e++; while (IsDigit(*e)) e++; char *a = NewStr(c, e - c); if (!a) break; Addr.Insert(a); c = e; } } else { // Correct data? Hopefully... #if 1 SafeAddrTokenize(vTo.Str(), Addr); #else TokeniseStrList(vTo.Str(), Addr, ","); #endif } for (auto r: Addr) { Store3Addr *a = new Store3Addr(GetStore()); if (a) { char *CcType = strrchr(r, ':'); if (CcType) *CcType++ = 0; LAutoString Name, Addr; DecodeAddrName(r, Name, Addr, 0); a->SetStr(FIELD_NAME, Name); a->SetStr(FIELD_EMAIL, Addr); a->CC = CcType ? atoi(CcType) : 0; To.Insert(a, -1, true); } } Addr.DeleteArrays(); To.State = Store3Loaded; } return true; } uint32_t LMail3Mail::Type() { return MAGIC_MAIL; } size_t Sizeof(LVariant &v) { int s = 0; switch (v.Type) { default: break; case GV_STRING: return (int)strlen(v.Str()); case GV_WSTRING: return StrlenW(v.WStr()) * sizeof(char16); } return s + sizeof(v); } size_t Sizeof(DIterator &i) { size_t s = sizeof(i); for (unsigned n=0; nSizeof(); return s; } uint64 LMail3Mail::Size() { if (MailSize < 0 && Seg) { MailSize = (uint32_t) (Seg->Size() + Seg->SizeChildren()); } return MailSize + // Size of mime segments... sizeof(Priority) + sizeof(Flags) + sizeof(AccountId) + Sizeof(Subject) + Sizeof(To) + From.Sizeof() + Reply.Sizeof() + Sizeof(Label) + // Sizeof(InternetHeader) + Sizeof(MessageID) + Sizeof(References) + Sizeof(FwdMsgId) + Sizeof(BounceMsgId) + Sizeof(ServerUid) + sizeof(DateReceived) + sizeof(DateSent); } bool LMail3Mail::DbDelete() { return Store->DeleteMailById(Id); } LDataStoreI *LMail3Mail::GetStore() { return Store; } LAutoStreamI LMail3Mail::GetStream(const char *file, int line) { LAutoStreamI Ret; const char *TmpPath = Store->GetStr(FIELD_TEMP_PATH); if (Flags & MAIL_STORED_FLAT) { // Construct from the single segment... LString Sql; Sql.Printf("select * from '%s' where MailId=" LPrintfInt64, MAIL3_TBL_MAILSEGS, Id); LMail3Store::LStatement s(Store, Sql); if (s.Row()) { LStreamConcat *Sc; int64 SegId = s.GetInt64(0); char *Hdr = s.GetStr(3); if (Hdr && Ret.Reset(Sc = new LStreamConcat)) { size_t HdrLen = strlen(Hdr); int BlobSize = s.GetSize(4); Sc->Add(new LMemStream(Hdr, HdrLen)); Sc->Add(new LMemStream("\r\n\r\n", 4)); Sc->Add(new Mail3BlobStream(Store, (int)SegId, BlobSize, file, line)); LFile f; if (f.Open("c:\\temp\\email.eml", O_WRITE)) { LCopyStreamer Cp(64<<10); Cp.Copy(Sc, &f); f.Close(); Sc->SetPos(0); } } } } else if (Ret.Reset(new LTempStream(TmpPath))) { // Encode from multiple segments... LoadSegs(); if (Seg) { LMime Mime(TmpPath); if (Store3ToLMime(&Mime, Seg)) { if (!Mime.Text.Encode.Push(Ret)) { LgiTrace("%s:%i - Mime encode failed.\n", _FL); Ret.Reset(); } } else { LgiTrace("%s:%i - Store3ToGMime failed.\n", _FL); Ret.Reset(); } } else { LgiTrace("%s:%i - No segment to encode.\n", _FL); Ret.Reset(); } } return Ret; } bool LMail3Mail::SetStream(LAutoStreamI stream) { if (!stream) return false; DeleteObj(Seg); // MIME parse the stream and store it to segments. LMime Mime; if (Mime.Text.Decode.Pull(stream)) { Seg = new LMail3Attachment(Store); if (Seg) { // This stops the objects being written to disk. // Which would mean we have multiple copies of the same // data on disk. This setting also propagates down the // tree automatically as GMimeToStore3 saves new child // notes to their parents. Seg->SetInMemoryOnly(true); Seg->AttachTo(this); GMimeToStore3(Seg, &Mime); } } return false; } bool LMail3Mail::FindSegs(const char *MimeType, LArray &Results, bool Create) { LoadSegs(); if ((!Seg || !Seg->FindSegs(MimeType, Results)) && Create) { LMail3Attachment *a = new LMail3Attachment(Store); if (!a) return false; a->SetStr(FIELD_MIME_TYPE, MimeType); Store3MimeTree Tree(this, Seg); Tree.Add(a); if (!Tree.Build()) return false; Results.Add(a); } /* for (unsigned i=0; iGetStr(FIELD_MIME_TYPE)) { // Ugh, a bug has caused a bunch of NULL mime-types.. a->SetStr(FIELD_MIME_TYPE, MimeType); } } */ return Results.Length() > 0; } LMail3Attachment *LMail3Mail::GetAttachment(int64 Id) { return Seg ? Seg->Find(Id) : 0; } struct Pair { int64 Parent; LMail3Attachment *Seg; }; int LMail3Mail::GetAttachments(LArray *Lst) { if (!Seg) return -1; int Count = 0; if (Seg->IsMultipart()) { LDataIt It = Seg->GetList(FIELD_MIME_SEG); if (It) { for (LDataPropI *i=It->First(); i; i=It->Next()) { const char *Mt = i->GetStr(FIELD_MIME_TYPE); const char *Name = i->GetStr(FIELD_NAME); if ( ( Mt && _stricmp("text/plain", Mt) && _stricmp("text/html", Mt) ) || ValidStr(Name) ) { Count++; if (Lst) { LMail3Attachment *a = dynamic_cast(i); if (a) Lst->Add(a); } } } } } return Count; } void LMail3Mail::LoadSegs() { if (Id > 0 && !Seg) { if (Flags & MAIL_STORED_FLAT) { // Decompression flat MIME storage to tree // GetStream will do the hard work of joining the message back // into RFC822 format. LAutoStreamI Rfc822 = GetStream(_FL); if (!Rfc822) return; // Then we MIME parse it and store it to segments. SetStream(Rfc822); } else { LArray Others; char Sql[256]; sprintf_s(Sql, sizeof(Sql), "select * from '%s' where MailId=" LPrintfInt64, MAIL3_TBL_MAILSEGS, Id); LMail3Store::LStatement s(Store, Sql); while (s.Row()) { Pair p; p.Seg = new LMail3Attachment(Store); if (p.Seg->Load(s, p.Parent)) { if (p.Parent <= 0) { if (Seg) { // hmmm, this shouldn't happen p.Seg->AttachTo(Seg); } else { p.Seg->AttachTo(this); } } else { Others.Add(p); } } } while (Seg && Others.Length()) { ssize_t StartSize = Others.Length(); for (unsigned i=0; iFind(Others[i].Parent); if (p) { Others[i].Seg->AttachTo(p); Others.DeleteAt(i--); } } if (StartSize == Others.Length()) { // No segments could be attached? Must have borked up parent id's while (Others.Length()) { // Just attach them somewhere... anywhere...! if (Seg) Others[0].Seg->AttachTo(Seg); else Others[0].Seg->AttachTo(this); Others.DeleteAt(0); } } } if (Seg) { int Attached = GetAttachments(); int NewFlags; if (Attached > 0) NewFlags = Flags | MAIL_ATTACHMENTS; else NewFlags = Flags & ~MAIL_ATTACHMENTS; if (NewFlags != Flags) { Flags = NewFlags; Save(); } } } } } static auto DefaultCharset = "windows-1252"; const char *LMail3Mail::GetStr(int id) { switch (id) { case FIELD_DEBUG: { static char s[64]; sprintf_s(s, sizeof(s), "Mail3.Id=" LPrintfInt64, Id); return s; } case FIELD_SUBJECT: { if (!LIsUtf8(Subject.Str())) { auto cs = DetectCharset(Subject.Str()); LAutoString conv; if (conv.Reset((char*)LNewConvertCp("utf-8", Subject.Str(), cs ? cs.Get() : DefaultCharset))) Subject = conv; } #if 0 int32 ch; LgiTrace("Subj:"); for (LUtf8Ptr p(Subject.Str()); ch = p; p++) LgiTrace(" u+%x", ch); LgiTrace("\n"); #endif return Subject.Str(); } case FIELD_CHARSET: { return "utf-8"; } case FIELD_TEXT: { LArray Results; if (!TextCache && FindSegs("text/plain", Results)) { LStringPipe p; for (auto seg: Results) { if (seg->GetStr(FIELD_NAME)) continue; auto s = seg->GetStream(_FL); if (!s) continue; LString Charset = seg->GetStr(FIELD_CHARSET); LAutoString Buf; auto Size = s->GetSize(); if (Size <= 0) continue; Buf.Reset(new char[Size+1]); s->Read(&Buf[0], Size); Buf[Size] = 0; if (!Charset) Charset = DetectCharset(Buf.Get()); if (!Charset) Charset = DefaultCharset; LAutoString Utf8; if (!Stricmp(Charset.Get(), "utf-8") && LIsUtf8(Buf)) Utf8 = Buf; else Utf8.Reset((char*)LNewConvertCp("utf-8", Buf, Charset, Size)); if (Results.Length() > 1) { if (p.GetSize()) p.Print("\n---------------------------------------------\n"); p.Push(Utf8); } else { TextCache = Utf8; return TextCache; } } TextCache.Reset(p.NewStr()); } return TextCache; } case FIELD_HTML_CHARSET: { return "utf-8"; } case FIELD_ALTERNATE_HTML: { LArray Results; if (!HtmlCache && FindSegs("text/html", Results)) { LMemQueue Blocks(1024); int64 Total = 0; for (unsigned i=0; iGetStr(FIELD_NAME)) { LAutoStreamI s = Results[i]->GetStream(_FL); if (s) { int64 Size = s->GetSize(); if (Size > 0) { auto Charset = Results[i]->GetStr(FIELD_CHARSET); char *Buf = new char[(size_t)Size+1]; ssize_t Rd = s->Read(Buf, (int)Size); if (Rd > 0) { Buf[Rd] = 0; if (Charset && _stricmp("utf-8", Charset) != 0) { char *Tmp = (char*)LNewConvertCp("utf-8", Buf, Charset, (int)Size); if (Tmp) { DeleteArray(Buf); Buf = Tmp; Size = strlen(Tmp); } } Blocks.Write(Buf, (int)Size); Total += Size; DeleteArray(Buf); } } } } } HtmlCache.Reset((char*)Blocks.New(1)); } return HtmlCache; } case FIELD_LABEL: return Label.Str(); case FIELD_INTERNET_HEADER: { auto Root = GetObj(FIELD_MIME_SEG); if (Root) return Root->GetStr(id); else return NULL; } case FIELD_REFERENCES: return References.Str(); case FIELD_FWD_MSG_ID: return FwdMsgId.Str(); case FIELD_BOUNCE_MSG_ID: return BounceMsgId.Str(); case FIELD_SERVER_UID: return ServerUid.Str(); case FIELD_MESSAGE_ID: { if (!MessageID.Str()) { LAutoString Header(InetGetHeaderField(GetStr(FIELD_INTERNET_HEADER), "Message-ID")); if (Header) { auto Ids = ParseIdList(Header); MessageID = Ids[0]; } } return MessageID.Str(); } case FIELD_UID: { IdCache.Printf(LPrintfInt64, Id); return IdCache; } } LAssert(0); return 0; } void LMail3Mail::OnSave() { if (Seg) { LDataStoreI::StoreTrans Trans = Store->StartTransaction(); Seg->OnSave(); } } void LMail3Mail::ParseAddresses(char *Str, int CC) { List Addr; TokeniseStrList(Str, Addr, ","); for (auto RawAddr: Addr) { LAutoPtr a(To.Create(Store)); if (a) { Store3Addr *sa = dynamic_cast(a.Get()); LAssert(sa != NULL); if (sa) { DecodeAddrName(RawAddr, sa->Name, sa->Addr, 0); sa->CC = CC; To.Insert(a.Release()); } } } Addr.DeleteArrays(); } void LMail3Mail::ResetCaches() { TextCache.Reset(); HtmlCache.Reset(); SizeCache.Reset(); } const char *LMail3Mail::InferCharset() { if (!InferredCharset) { // Sometimes mailers don't follow the rules... *cough*outlook*cough* // So lets play the "guess the charset" game... // Maybe one of the segments has a charset? InferredCharset = Seg->GetStr(FIELD_CHARSET); if (!InferredCharset) { // What about 'Content-Language'? auto InetHdrs = GetStr(FIELD_INTERNET_HEADER); if (InetHdrs) { LAutoString ContentLang(InetGetHeaderField(InetHdrs, "Content-Language")); if (ContentLang) { LLanguage *Lang = LFindLang(ContentLang); if (Lang) InferredCharset = Lang->Charset; } } } } return InferredCharset; } bool LMail3Mail::Utf8Check(LAutoString &v) { if (!LIsUtf8(v.Get())) { const char *Cs = InferCharset(); if (Cs) { LAutoString Value((char*) LNewConvertCp("utf-8", v, Cs, -1)); if (Value) { v = Value; return true; } } } return false; } bool LMail3Mail::Utf8Check(LVariant &v) { if (!LIsUtf8(v.Str())) { const char *Cs = InferCharset(); if (Cs) { LAutoString Value((char*) LNewConvertCp("utf-8", v.Str(), Cs, -1)); if (Value) { v.OwnStr(Value.Release()); return true; } } } return false; } bool LMail3Mail::ParseHeaders() { // Reload from headers... auto InetHdrs = GetStr(FIELD_INTERNET_HEADER); Subject.OwnStr(DecodeRfc2047(InetGetHeaderField(InetHdrs, "subject"))); Utf8Check(Subject); // From LAutoString s(DecodeRfc2047(InetGetHeaderField(InetHdrs, "from"))); Utf8Check(s); From.Empty(); DecodeAddrName(s, From.Name, From.Addr, NULL); s.Reset(DecodeRfc2047(InetGetHeaderField(InetHdrs, "reply-to"))); Utf8Check(s); Reply.Empty(); DecodeAddrName(s, Reply.Name, Reply.Addr, NULL); // Parse To and CC headers. To.DeleteObjects(); if (s.Reset(DecodeRfc2047(InetGetHeaderField(InetHdrs, "to")))) { Utf8Check(s); ParseAddresses(s, MAIL_ADDR_TO); } if (s.Reset(DecodeRfc2047(InetGetHeaderField(InetHdrs, "cc")))) { Utf8Check(s); ParseAddresses(s, MAIL_ADDR_CC); } // Data if (s.Reset(InetGetHeaderField(InetHdrs, "date"))) { DateSent.Decode(s); DateSent.ToUtc(); } DeleteObj(Seg); ResetCaches(); return true; } Store3Status LMail3Mail::SetStr(int id, const char *str) { switch (id) { case FIELD_SUBJECT: Subject = str; break; case FIELD_TEXT: { TextCache.Reset(); LArray Results; if (FindSegs("text/plain", Results, str != 0)) { for (unsigned i=0; iGetStr(FIELD_NAME)) { LAutoStreamI s(str ? new LMemStream((char*)str, strlen(str)) : 0); Results[i]->SetStream(s); break; } } } break; } case FIELD_CHARSET: { LArray Results; if (FindSegs("text/plain", Results, str != 0)) { for (unsigned i=0; iGetStr(FIELD_NAME)) { Results[i]->SetStr(FIELD_CHARSET, str); break; } } } break; } case FIELD_ALTERNATE_HTML: { HtmlCache.Reset(); LArray Results; if (FindSegs("text/html", Results, str != 0)) { for (unsigned i=0; iGetStr(FIELD_NAME)) { if (str) { LAutoStreamI tmp(new LMemStream((char*)str, strlen(str))); Results[i]->SetStream(tmp); } else { auto s = Results[i]; s->Delete(); delete s; } break; } } } break; } case FIELD_HTML_CHARSET: { LArray Results; if (FindSegs("text/html", Results, str != 0)) { const char *Charset = str && *str == '>' ? str + 1 : str; for (unsigned i=0; iGetStr(FIELD_NAME)) { if (Results[i]->SetStr(FIELD_CHARSET, Charset)) return Store3Success; break; } } } return Store3Error; } case FIELD_LABEL: Label = str; break; case FIELD_INTERNET_HEADER: { LoadSegs(); if (!ValidStr(str)) // There is no point continuing as it will just attach an empty // attachment which eventually asserts in the save code. // e.g. LMail3Attachment::GetHeaders() break; if (!Seg) { // This happens when the user re-sends an email and it creates // a new empty email to copy the old sent email into. LMail3Attachment *a = new LMail3Attachment(Store); if (!a) { LAssert(0); return Store3Error; } a->AttachTo(this); } Seg->SetStr(id, str); break; } case FIELD_MESSAGE_ID: MessageID = str; break; case FIELD_REFERENCES: References = str; break; case FIELD_FWD_MSG_ID: FwdMsgId = str; break; case FIELD_BOUNCE_MSG_ID: BounceMsgId = str; break; case FIELD_SERVER_UID: ServerUid = str; break; default: LAssert(0); return Store3Error; } return Store3Success; } int64 LMail3Mail::GetInt(int id) { switch (id) { case FIELD_STORE_TYPE: return Store3Sqlite; case FIELD_SIZE: return Size(); case FIELD_LOADED: return Store3Loaded; case FIELD_PRIORITY: return Priority; case FIELD_FLAGS: return Flags; case FIELD_DONT_SHOW_PREVIEW: return false; case FIELD_ACCOUNT_ID: return AccountId; case FIELD_COLOUR: return (uint64_t)MarkColour; case FIELD_SERVER_UID: return -1; } LAssert(0); return -1; } Store3Status LMail3Mail::SetInt(int id, int64 i) { switch (id) { case FIELD_LOADED: return Store3Success; case FIELD_PRIORITY: Priority = (int)i; return Store3Success; case FIELD_FLAGS: Flags = (int)i; return Store3Success; case FIELD_ACCOUNT_ID: AccountId = (int)i; return Store3Success; case FIELD_COLOUR: if (i < 0) MarkColour = Rgba32(0, 0, 0, 0); // transparent else MarkColour = (uint32_t)i; return Store3Success; } LAssert(0); return Store3NotImpl; } const LDateTime *LMail3Mail::GetDate(int id) { switch (id) { case FIELD_DATE_RECEIVED: return &DateReceived; case FIELD_DATE_SENT: return &DateSent; } LAssert(0); return 0; } Store3Status LMail3Mail::SetDate(int id, const LDateTime *t) { switch (id) { case FIELD_DATE_RECEIVED: if (t) DateReceived = *t; else DateReceived.Year(0); return Store3Success; case FIELD_DATE_SENT: if (t) DateSent = *t; else DateSent.Year(0); return Store3Success; } LAssert(0); return Store3NotImpl; } LDataPropI *LMail3Mail::GetObj(int id) { switch (id) { case FIELD_FROM: return &From; case FIELD_REPLY: return &Reply; case FIELD_MIME_SEG: { LoadSegs(); /* This causes replies to have the wrong format for "text/plain" if (!Seg) { LMail3Attachment *a = new LMail3Attachment(Store); if (a) { a->SetStr(FIELD_MIME_TYPE, sMultipartMixed); a->AttachTo(this); } } */ return Seg; } } LAssert(0); return 0; } Store3Status LMail3Mail::SetObj(int id, LDataPropI *i) { switch (id) { case FIELD_MIME_SEG: { if (Seg) { Seg->SetMail(NULL); Seg = NULL; } LMail3Attachment *a = dynamic_cast(i); if (!a) { LAssert(!"Incorrect object..."); return Store3Error; } Seg = a; Seg->SetMail(this); break; } case FIELD_HTML_RELATED: { LMail3Attachment *a = i ? dynamic_cast(i) : NULL; LoadSegs(); if (Seg) { Store3MimeTree Tree(this, Seg); if (a) Tree.MsgHtmlRelated.Add(a); else Tree.MsgHtmlRelated.Empty(); if (!Tree.Build()) return Store3Error; MailSize = -1; Seg->Save(this); } break; } default: LAssert(0); return Store3NotImpl; } return Store3Success; } LDataIt LMail3Mail::GetList(int id) { switch (id) { case FIELD_TO: return &To; } LAssert(0); return 0; } class LSubStream : public LStreamI { // The source stream to read from LStreamI *s; // The start position in the source stream int64 Start; // The length of the sub-stream int64 Len; // The current position in the sub-stream // (relative to the start of the sub-stream, not the parent stream) int64 Pos; public: LSubStream(LStreamI *stream, int64 start = 0, int64 len = -1) { s = stream; Pos = 0; Start = MAX(0, start); int64 MaxLen = s->GetSize() - Start; if (MaxLen < 0) MaxLen = 0; Len = len < 0 && s ? MaxLen : MIN(MaxLen, len); } bool IsOpen() { return true; } int Close() { s = NULL; Start = Len = Pos = 0; return true; } int64 GetSize() { return s ? Len : -1; } int64 SetSize(int64 Size) { // Can't set size return GetSize(); } int64 GetPos() { return s ? Pos : -1; } int64 SetPos(int64 p) { if (p < 0) p = 0; if (p >= Len) p = Len; return Pos = p; } ssize_t Read(void *Buffer, ssize_t Size, int Flags = 0) { ssize_t r = 0; if (s && Buffer) { int64 Remaining = Len - Pos; ssize_t Common = MIN(Size, (int)Remaining); int64 SrcPos = Start + Pos; int64 ActualPos = s->SetPos(SrcPos); if (ActualPos == SrcPos) { r = s->Read(Buffer, Common, Flags); if (r > 0) { Pos += r; } } else LAssert(0); } return r; } ssize_t Write(const void *Buffer, ssize_t Size, int Flags = 0) { LAssert(!"Not implemented."); return 0; } LStreamI *Clone() { return new LSubStream(s, Start, Len); } }; Store3Status LMail3Mail::SetRfc822(LStreamI *m) { Store3Status Status = Store3Error; if (m) { // Save all the segments out LDataStoreI::StoreTrans Trans = Store->StartTransaction(); if (Id < 0) { // Must commit ourselves now and get an Id if (!Write(MAIL3_TBL_MAIL, true)) return Store3Error; } else { char s[256]; sprintf_s(s, sizeof(s), "delete from %s where MailId=" LPrintfInt64, MAIL3_TBL_MAILSEGS, Id); LMail3Store::LStatement Del(Store, s); if (!Del.Exec()) return Store3Error; } DeleteObj(Seg); /* Normally the message is parsed into MIME segments and stored parsed and decoded into the MailSegs table. If the message is encrypted or signed that could the client can't verify the message later because the specifics of MIME encoding varies between clients. So if the message is: Signed and/or Encrypted: There is only one MAILSEG record with all the headers of the root MIME segment, and the body stream has all the data of the RFC822 image. Otherwise: Normal MIME parsing is done, storing the message in different MAILSEG records. Which was the previous behaviour. First the headers of the root MIME node are read to see what the Content-Type is. Then a decision about how to store the node is made. */ LString Hdrs = HeadersFromStream(m); LAutoString Type(InetGetHeaderField(Hdrs, "Content-Type")); Flags &= ~MAIL_STORED_FLAT; if (Type) { LString s = Type.Get(); ptrdiff_t Colon = s.Find(";"); if (Colon > 0) s.Length((uint32_t)Colon); s = s.Strip().Lower(); if (s == sMultipartEncrypted || s == sMultipartSigned) { Flags |= MAIL_STORED_FLAT; } } LMail3Store::LInsert Ins(Store, MAIL3_TBL_MAILSEGS); MailSize = 0; LMime Mime; if (Flags & MAIL_STORED_FLAT) { // Don't parse MIME into a tree. Mime.SetHeaders(Hdrs); Mime.SetData(true, new LSubStream(m, Hdrs.Length()+4)); if (Mail3_InsertSeg(Ins, &Mime, Id, -1, MailSize)) Status = Store3Success; } else { // Do normal parsing in to MIME tree. if (Mime.Text.Decode.Pull(m) && Mail3_InsertSeg(Ins, &Mime, Id, -1, MailSize)) Status = Store3Success; } } return Status; }