diff --git a/Lvc/Src/Main.cpp b/Lvc/Src/Main.cpp --- a/Lvc/Src/Main.cpp +++ b/Lvc/Src/Main.cpp @@ -1,726 +1,734 @@ #include "Lgi.h" #include "../Resources/resdefs.h" #include "Lvc.h" #include "GTableLayout.h" #ifdef WINDOWS #include "../Resources/resource.h" #endif #include "GTextLog.h" #include "GButton.h" #include "GXmlTreeUi.h" ////////////////////////////////////////////////////////////////// const char *AppName = "Lvc"; VersionCtrl DetectVcs(const char *Path) { char p[MAX_PATH]; if (!Path) return VcNone; if (LgiMakePath(p, sizeof(p), Path, ".git") && DirExists(p)) return VcGit; if (LgiMakePath(p, sizeof(p), Path, ".svn") && DirExists(p)) return VcSvn; if (LgiMakePath(p, sizeof(p), Path, ".hg") && DirExists(p)) return VcHg; if (LgiMakePath(p, sizeof(p), Path, "CVS") && DirExists(p)) return VcCvs; return VcNone; } class DiffView : public GTextLog { public: DiffView(int id) : GTextLog(id) { } void PourStyle(size_t Start, ssize_t Length) { for (auto ln : GTextView3::Line) { if (!ln->c.IsValid()) { char16 *t = Text + ln->Start; if (*t == '+') { ln->c = GColour::Green; ln->Back.Rgb(245, 255, 245); } else if (*t == '-') { ln->c = GColour::Red; ln->Back.Rgb(255, 245, 245); } else if (*t == '@') { ln->c.Rgb(128, 128, 128); ln->Back.Rgb(235, 235, 235); } else ln->c.Set(LC_TEXT, 24); } } } }; class ToolBar : public GLayout, public GLgiRes { public: ToolBar() { GAutoString Name; GRect Pos; if (LoadFromResource(IDD_TOOLBAR, this, &Pos, &Name)) { OnPosChange(); } else LgiAssert(!"Missing toolbar resource"); } void OnCreate() { AttachChildren(); } void OnPosChange() { GRect Cli = GetClient(); GTableLayout *v; if (GetViewById(IDC_TABLE, v)) { v->SetPos(Cli); v->OnPosChange(); auto r = v->GetUsedArea(); if (r.Y() <= 1) r.Set(0, 0, 30, 30); - GetCss(true)->Height(GCss::Len(GCss::LenPx, (float)r.Y()+3)); + + // printf("Used = %s\n", r.GetStr()); + GCss::Len NewSz(GCss::LenPx, (float)r.Y()+3); + auto OldSz = GetCss(true)->Height(); + if (OldSz != NewSz) + { + GetCss(true)->Height(NewSz); + SendNotify(GNotifyTableLayout_Refresh); + } } else LgiAssert(!"Missing table ctrl"); } void OnPaint(GSurface *pDC) { pDC->Colour(LC_MED, 24); pDC->Rectangle(); } }; class CommitCtrls : public GLayout, public GLgiRes { public: CommitCtrls() { GAutoString Name; GRect Pos; if (LoadFromResource(IDD_COMMIT, this, &Pos, &Name)) { GTableLayout *v; if (GetViewById(IDC_COMMIT_TABLE, v)) { v->GetCss(true)->PaddingRight("8px"); GRect r = v->GetPos(); r.Offset(-r.x1, -r.y1); r.x2++; v->SetPos(r); v->OnPosChange(); r = v->GetUsedArea(); if (r.Y() <= 1) r.Set(0, 0, 30, 30); GetCss(true)->Height(GCss::Len(GCss::LenPx, (float)r.Y())); } else LgiAssert(!"Missing table ctrl"); } else LgiAssert(!"Missing toolbar resource"); } void OnPosChange() { GTableLayout *v; if (GetViewById(IDC_COMMIT_TABLE, v)) v->SetPos(GetClient()); } void OnCreate() { AttachChildren(); } }; GString::Array GetProgramsInPath(const char *Program) { GString::Array Bin; GString Prog = Program; #ifdef WINDOWS Prog += LGI_EXECUTABLE_EXT; #endif GString::Array a = LGetPath(); for (auto p : a) { GFile::Path c(p, Prog); if (c.Exists()) Bin.New() = c.GetFull(); } return Bin; } class OptionsDlg : public GDialog, public GXmlTreeUi { GOptionsFile &Opts; public: OptionsDlg(GViewI *Parent, GOptionsFile &opts) : Opts(opts) { SetParent(Parent); Map("svn-path", IDC_SVN, GV_STRING); Map("git-path", IDC_GIT, GV_STRING); Map("hg-path", IDC_HG, GV_STRING); Map("cvs-path", IDC_CVS, GV_STRING); if (LoadFromResource(IDD_OPTIONS)) { MoveSameScreen(Parent); Convert(&Opts, this, true); } } void Browse(int EditId) { GFileSelect s; s.Parent(this); if (s.Open()) { SetCtrlName(EditId, s.Name()); } } void BrowseFiles(GViewI *Ctrl, const char *Bin, int EditId) { GRect Pos = Ctrl->GetPos(); GdcPt2 Pt(Pos.x1, Pos.y2 + 1); PointToScreen(Pt); GSubMenu s; GString::Array Bins = GetProgramsInPath(Bin); for (unsigned i=0; i= 1000) { GString Bin = Bins[Cmd - 1000]; if (Bin) SetCtrlName(EditId, Bin); } break; } } } int OnNotify(GViewI *Ctrl, int Flags) { switch (Ctrl->GetId()) { case IDC_SVN_BROWSE: BrowseFiles(Ctrl, "svn", IDC_SVN); break; case IDC_GIT_BROWSE: BrowseFiles(Ctrl, "git", IDC_GIT); break; case IDC_HG_BROWSE: BrowseFiles(Ctrl, "hg", IDC_HG); break; case IDC_CVS_BROWSE: BrowseFiles(Ctrl, "cvs", IDC_CVS); break; case IDOK: Convert(&Opts, this, false); // fall case IDCANCEL: { EndModal(Ctrl->GetId() == IDOK); break; } } return GDialog::OnNotify(Ctrl, Flags); } }; class CommitList : public LList { public: CommitList(int id) : LList(id, 0, 0, 200, 200) { } void SelectRevisions(GString::Array &Revs) { VcCommit *Scroll = NULL; for (auto it = begin(); it != end(); it++) { VcCommit *item = dynamic_cast(*it); if (item) { for (auto r: Revs) { if (item->IsRev(r)) { if (!Scroll) Scroll = item; item->Select(true); } } } } if (Scroll) Scroll->ScrollTo(); } bool OnKey(GKey &k) { switch (k.c16) { case 'p': case 'P': { if (k.Down()) { GArray Sel; GetSelection(Sel); if (Sel.Length()) { auto p = Sel[0]->GetParents(); if (p->Length() == 0) break; for (auto c:Sel) c->Select(false); SelectRevisions(*p); } } return true; } } return LList::OnKey(k); } }; class App : public GWindow, public AppPriv { GAutoPtr ImgLst; public: App() { GString AppRev; AppRev.Printf("%s v%s", AppName, APP_VERSION); Name(AppRev); GRect r(0, 0, 1400, 800); SetPos(r); MoveToCenter(); SetQuitOnClose(true); Opts.SerializeFile(false); SerializeState(&Opts, "WndPos", true); #ifdef WINDOWS SetIcon(MAKEINTRESOURCEA(IDI_ICON1)); #else SetIcon("icon32.png"); #endif ImgLst.Reset(LgiLoadImageList("image-list.png", 16, 16)); if (Attach(0)) { if ((Menu = new GMenu)) { Menu->SetPrefAndAboutItems(IDM_OPTIONS, IDM_ABOUT); Menu->Attach(this); Menu->Load(this, "IDM_MENU"); } GBox *ToolsBox = new GBox(IDC_TOOLS_BOX, true, "ToolsBox"); GBox *FoldersBox = new GBox(IDC_FOLDERS_BOX, false, "FoldersBox"); GBox *CommitsBox = new GBox(IDC_COMMITS_BOX, true, "CommitsBox"); ToolBar *Tools = new ToolBar; ToolsBox->Attach(this); Tools->Attach(ToolsBox); FoldersBox->Attach(ToolsBox); Tree = new GTree(IDC_TREE, 0, 0, 200, 200); Tree->GetCss(true)->Width(GCss::Len("320px")); Tree->ShowColumnHeader(true); Tree->AddColumn("Folder", 250); Tree->AddColumn("Counts", 50); Tree->Attach(FoldersBox); Tree->SetImageList(ImgLst, false); CommitsBox->Attach(FoldersBox); Lst = new CommitList(IDC_LIST); Lst->Attach(CommitsBox); Lst->GetCss(true)->Height("40%"); Lst->AddColumn("---", 40); Lst->AddColumn("Commit", 270); Lst->AddColumn("Author", 240); Lst->AddColumn("Date", 130); Lst->AddColumn("Message", 400); GBox *FilesBox = new GBox(IDC_FILES_BOX, false); FilesBox->Attach(CommitsBox); Files = new LList(IDC_FILES, 0, 0, 200, 200); Files->GetCss(true)->Width("35%"); Files->Attach(FilesBox); Files->AddColumn("[ ]", 30); Files->AddColumn("State", 100); Files->AddColumn("Name", 400); GBox *MsgBox = new GBox(IDC_MSG_BOX, true); MsgBox->Attach(FilesBox); CommitCtrls *Commit = new CommitCtrls; Commit->Attach(MsgBox); Commit->GetCss(true)->Height("25%"); if (Commit->GetViewById(IDC_MSG, Msg)) { GTextView3 *Tv = dynamic_cast(Msg); if (Tv) { Tv->Sunken(true); Tv->SetWrapType(TEXTED_WRAP_NONE); } } else LgiAssert(!"No ctrl?"); Tabs = new GTabView(IDC_TAB_VIEW); Tabs->Attach(MsgBox); const char *Style = "Padding: 0px 8px 8px 0px"; Tabs->GetCss(true)->Parse(Style); GTabPage *p = Tabs->Append("Diff"); p->Append(Diff = new DiffView(IDC_TXT)); // Diff->Sunken(true); Diff->SetWrapType(TEXTED_WRAP_NONE); p = Tabs->Append("Log"); p->Append(Log = new GTextLog(IDC_LOG)); // Log->Sunken(true); Log->SetWrapType(TEXTED_WRAP_NONE); AttachChildren(); Visible(true); } GXmlTag *f = Opts.LockTag(OPT_Folders, _FL); if (!f) { Opts.CreateTag(OPT_Folders); f = Opts.LockTag(OPT_Folders, _FL); } if (f) { bool Req[VcMax] = {0}; for (GXmlTag *c = f->Children.First(); c; c = f->Children.Next()) { if (c->IsTag(OPT_Folder)) { auto f = new VcFolder(this, c); Tree->Insert(f); if (!Req[f->GetType()]) { Req[f->GetType()] = true; f->GetVersion(); } } } Opts.Unlock(); } SetPulse(200); } ~App() { SerializeState(&Opts, "WndPos", false); GXmlTag *f = Opts.LockTag(OPT_Folders, _FL); if (f) { f->EmptyChildren(); VcFolder *vcf = NULL; bool b; while ((b = Tree->Iterate(vcf))) f->InsertTag(vcf->Save()); Opts.Unlock(); } Opts.SerializeFile(true); } int OnCommand(int Cmd, int Event, OsView Wnd) { switch (Cmd) { case IDM_OPTIONS: { OptionsDlg Dlg(this, Opts); Dlg.DoModal(); break; } case IDM_FIND: { GInput i(this, "", "Search string:"); if (i.DoModal()) { GString::Array Revs; Revs.Add(i.GetStr()); CommitList *cl; if (GetViewById(IDC_LIST, cl)) cl->SelectRevisions(Revs); } break; } } return 0; } void OnPulse() { VcFolder *vcf = NULL; bool b; while ((b = Tree->Iterate(vcf))) vcf->OnPulse(); } void OpenFolder() { GFileSelect s; s.Parent(this); if (s.OpenFolder()) { if (DetectVcs(s.Name())) Tree->Insert(new VcFolder(this, s.Name())); else LgiMsg(this, "Folder not under version control.", AppName); } } int OnNotify(GViewI *c, int flag) { switch (c->GetId()) { case IDC_FILES: { switch (flag) { case GNotifyItem_ColumnClicked: { int Col = -1; GMouse m; if (Files->GetColumnClickInfo(Col, m)) { if (Col == 0) { // Select / deselect all checkboxes.. List n; if (Files->GetAll(n)) { bool Checked = false; for (VcFile *f = n.First(); f; f = n.Next()) Checked |= f->Checked() > 0; for (VcFile *f = n.First(); f; f = n.Next()) f->Checked(Checked ? 0 : 1); } } } break; } } break; } case IDC_OPEN: { OpenFolder(); break; } case IDC_TREE: { switch (flag) { case GNotifyContainer_Click: { GMouse m; c->GetMouse(m); if (m.Right()) { GSubMenu s; s.AppendItem("Add", IDM_ADD); int Cmd = s.Float(c->GetGView(), m); switch (Cmd) { case IDM_ADD: { OpenFolder(); break; } } } break; } case LvcCommandStart: { SetCtrlEnabled(IDC_PUSH, false); SetCtrlEnabled(IDC_PUSH_ALL, false); SetCtrlEnabled(IDC_PULL, false); SetCtrlEnabled(IDC_PULL_ALL, false); break; } case LvcCommandEnd: { SetCtrlEnabled(IDC_PUSH, true); SetCtrlEnabled(IDC_PUSH_ALL, true); SetCtrlEnabled(IDC_PULL, true); SetCtrlEnabled(IDC_PULL_ALL, true); break; } } break; } case IDC_CLEAR_FILTER: { SetCtrlName(IDC_FILTER, NULL); // Fall through } case IDC_FILTER: { VcFolder *f = dynamic_cast(Tree->Selection()); if (f) f->Select(true); break; } case IDC_COMMIT_AND_PUSH: case IDC_COMMIT: { const char *Msg = GetCtrlName(IDC_MSG); if (ValidStr(Msg)) { VcFolder *f = dynamic_cast(Tree->Selection()); if (f) { char *Branch = GetCtrlName(IDC_BRANCH); bool AndPush = c->GetId() == IDC_COMMIT_AND_PUSH; f->Commit(Msg, ValidStr(Branch) ? Branch : NULL, AndPush); } } else LgiMsg(this, "No message for commit.", AppName); break; } case IDC_PUSH: { VcFolder *f = dynamic_cast(Tree->Selection()); if (f) f->Push(); break; } case IDC_PULL: { VcFolder *f = dynamic_cast(Tree->Selection()); if (f) f->Pull(); break; } case IDC_PULL_ALL: { GArray Folders; Tree->GetAll(Folders); for (auto f : Folders) { f->Pull(LogSilo); } break; } case IDC_STATUS: { GArray Folders; Tree->GetAll(Folders); for (auto f : Folders) { f->FolderStatus(); } break; } case IDC_HEADS: { if (flag == GNotifyValueChanged) { auto Revs = GString(c->Name()).SplitDelimit(); CommitList *cl; if (GetViewById(IDC_LIST, cl)) cl->SelectRevisions(Revs); } break; } } return 0; } }; ////////////////////////////////////////////////////////////////// int LgiMain(OsAppArguments &AppArgs) { GApp a(AppArgs, AppName); if (a.IsOk()) { a.AppWnd = new App; a.Run(); } return 0; } diff --git a/Lvc/Src/VcCommit.cpp b/Lvc/Src/VcCommit.cpp --- a/Lvc/Src/VcCommit.cpp +++ b/Lvc/Src/VcCommit.cpp @@ -1,412 +1,407 @@ #include "Lvc.h" #include "GClipBoard.h" #include "../Resources/resdefs.h" #include "GPath.h" #include "LHashTable.h" const char *sPalette[] = { "9696ff", "5D6D66", "4E3A35", "68B3C5", "9A92B0", "462D4C", "C5A378", "302D65" }; GColour GetPaletteColour(int i) { GColour c; const char *s = sPalette[i % CountOf(sPalette)]; #define Comp(comp, off) { const char h[3] = {s[off], s[off+1], 0}; c.comp(htoi(h)); } Comp(r, 0); Comp(g, 2); Comp(b, 4); return c; } VcEdge::~VcEdge() { if (Parent) Parent->Edges.Delete(this); if (Child) Child->Edges.Delete(this); } void VcEdge::Detach(VcCommit *c) { if (Parent == c) Parent = NULL; if (Child == c) Child = NULL; if (Parent == NULL && Child == NULL) delete this; } void VcEdge::Set(VcCommit *p, VcCommit *c) { if ((Parent = p)) Parent->Edges.Add(this); if ((Child = c)) Child->Edges.Add(this); } VcCommit::VcCommit(AppPriv *priv, VcFolder *folder) : Pos(32, -1) { d = priv; Folder = folder; Current = false; NodeIdx = -1; NodeColour = GetPaletteColour(0); Parents.SetFixedLength(false); } VcCommit::~VcCommit() { for (auto e: Edges) e->Detach(this); } char *VcCommit::GetRev() { return Rev; } char *VcCommit::GetAuthor() { return Author; } char *VcCommit::GetMsg() { return Msg; } void VcCommit::SetCurrent(bool b) { Current = b; } void VcCommit::OnPaintColumn(GItem::ItemPaintCtx &Ctx, int i, GItemColumn *c) { LListItem::OnPaintColumn(Ctx, i, c); if (i == 0) { double Px = 12; int Ht = Ctx.Y(); double Half = 5.5; #define MAP(col) ((col) * Px + Half) GMemDC Mem(Ctx.X(), Ctx.Y(), System32BitColourSpace); double r = Half - 1; double x = MAP(NodeIdx); Mem.Colour(GColour::Black); VcCommit *Prev = NULL, *Next = NULL; Prev = Folder->Log.IdxCheck(Idx - 1) ? Folder->Log[Idx - 1] : NULL; Next = Folder->Log.IdxCheck(Idx + 1) ? Folder->Log[Idx + 1] : NULL; for (auto it: Pos) { VcEdge *e = it.key; int CurIdx = it.value; if (CurIdx < 0) { continue; } double CurX = MAP(CurIdx); #define I(v) ((int)(v)) if (e->Child != this) { // Line to previous commit int PrevIdx = Prev ? Prev->Pos.Find(e) : -1; if (PrevIdx >= 0) { double PrevX = MAP(PrevIdx); Mem.Line(I(PrevX), I(-(Ht/2)), I(CurX), I(Ht/2)); } else { Mem.Colour(GColour::Red); Mem.Line(I(CurX), I(Ht/2), I(CurX), I(Ht/2-5)); Mem.Colour(GColour::Black); } } if (e->Parent != this) { int NextIdx = Next ? Next->Pos.Find(e) : -1; if (NextIdx >= 0) { double NextX = MAP(NextIdx); Mem.Line(I(NextX), I(Ht+(Ht/2)), I(CurX), I(Ht/2)); } else { Mem.Colour(GColour::Red); Mem.Line(I(CurX), I(Ht/2), I(CurX), I(Ht/2+5)); Mem.Colour(GColour::Black); } } } if (NodeIdx >= 0) { double Cx = x; double Cy = Ht / 2; { GPath p; p.Circle(Cx, Cy, r + (Current ? 1 : 0)); GSolidBrush sb(GColour::Black); p.Fill(&Mem, sb); } { GPath p; p.Circle(Cx, Cy, r-1); GSolidBrush sb(NodeColour); p.Fill(&Mem, sb); } } // Mem.ConvertPreMulAlpha(false); Ctx.pDC->Op(GDC_ALPHA); Ctx.pDC->Blt(Ctx.x1, Ctx.y1, &Mem); } } char *VcCommit::GetText(int Col) { switch (Col) { case 0: // Cache.Printf("%i%s", (int)Parents.Length(), Current ? " ***" : ""); return NULL; case 1: return Rev; case 2: return Author; case 3: Cache = Ts.Get(); return Cache; case 4: if (!Msg) return NULL; Cache = Msg.Split("\n", 1)[0]; return Cache; } return NULL; } bool VcCommit::GitParse(GString s, bool RevList) { GString::Array lines = s.Split("\n"); if (lines.Length() < 3) return false; if (RevList) { auto a = lines[0].SplitDelimit(); if (a.Length() != 2) return false; - #ifdef WINDOWS - // Unix timestamp to windows ticks - Ts.Set((uint64)a[0].Int() * LDateTime::Second64Bit + 116445168000000000LL); - #else - Ts.Set((uint64) a[0].Int()); - #endif + Ts.SetUnix((uint64) a[0].Int()); Rev = a[1]; for (int i=0; i= 0) Author = l.Split(":", 1)[1].Strip(); else if (l.Find("Date:") >= 0) Ts.Parse(l.Split(":", 1)[1].Strip()); else if (l.Strip().Length() > 0) { if (Msg) Msg += "\n"; Msg += l.Strip(); } } } return Author && Rev; } bool VcCommit::CvsParse(LDateTime &Dt, GString Auth, GString Message) { Ts = Dt; Ts.ToLocal(); uint64 i; if (Ts.Get(i)) Rev.Printf(LPrintfInt64, i); Author = Auth; Msg = Message; return true; } bool VcCommit::HgParse(GString s) { GString::Array Lines = s.SplitDelimit("\n"); if (Lines.Length() < 1) return false; for (GString *Ln = NULL; Lines.Iterate(Ln); ) { GString::Array f = Ln->Split(":", 1); if (f.Length() == 2) { if (f[0].Equals("changeset")) Rev = f[1].Strip(); else if (f[0].Equals("user")) Author = f[1].Strip(); else if (f[0].Equals("date")) Ts.Parse(f[1].Strip()); else if (f[0].Equals("summary")) Msg = f[1].Strip(); } } return Rev.Get() != NULL; } bool VcCommit::SvnParse(GString s) { GString::Array lines = s.Split("\n"); if (lines.Length() < 1) return false; for (unsigned ln = 0; ln < lines.Length(); ln++) { GString &l = lines[ln]; if (ln == 0) { GString::Array a = l.Split("|"); if (a.Length() > 3) { Rev = a[0].Strip(" \tr"); Author = a[1].Strip(); Ts.Parse(a[2]); } } else { if (Msg) Msg += "\n"; Msg += l.Strip(); } } Msg = Msg.Strip(); return Author && Rev && Ts.IsValid(); } VcFolder *VcCommit::GetFolder() { for (GTreeItem *i = d->Tree->Selection(); i; i = i->GetParent()) { auto f = dynamic_cast(i); if (f) return f; } return NULL; } void VcCommit::Select(bool b) { LListItem::Select(b); if (Rev && b) { VcFolder *f = GetFolder(); if (f) f->ListCommit(this); if (d->Msg) { d->Msg->Name(Msg); GWindow *w = d->Msg->GetWindow(); if (w) { w->SetCtrlEnabled(IDC_COMMIT, false); w->SetCtrlEnabled(IDC_COMMIT_AND_PUSH, false); } } else LgiAssert(0); } } void VcCommit::OnMouseClick(GMouse &m) { LListItem::OnMouseClick(m); if (m.IsContextMenu()) { GSubMenu s; s.AppendItem("Merge With Local", IDM_MERGE, !Current); s.AppendItem("Update", IDM_UPDATE, !Current); s.AppendItem("Copy Revision", IDM_COPY_REV); int Cmd = s.Float(GetList(), m); switch (Cmd) { case IDM_MERGE: { VcFolder *f = GetFolder(); if (!f) { LgiAssert(!"No folder?"); break; } f->MergeToLocal(Rev); break; } case IDM_UPDATE: { VcFolder *f = GetFolder(); if (!f) { LgiAssert(!"No folder?"); break; } f->OnUpdate(Rev); break; } case IDM_COPY_REV: { GClipBoard c(GetList()); c.Text(Rev); break; } } } } diff --git a/Lvc/Src/VcFolder.cpp b/Lvc/Src/VcFolder.cpp --- a/Lvc/Src/VcFolder.cpp +++ b/Lvc/Src/VcFolder.cpp @@ -1,2707 +1,2714 @@ #include "Lvc.h" #include "../Resources/resdefs.h" #include "GCombo.h" #ifndef CALL_MEMBER_FN #define CALL_MEMBER_FN(object,ptrToMember) ((object).*(ptrToMember)) #endif int Ver2Int(GString v) { auto p = v.Split("."); int i = 0; for (auto s : p) { auto Int = s.Int(); if (Int < 256) { i <<= 8; i |= (uint8_t)Int; } else { LgiAssert(0); return 0; } } return i; } int ToolVersion[VcMax] = {0}; ReaderThread::ReaderThread(GSubProcess *p, GStream *out) : LThread("ReaderThread") { Process = p; Out = out; Run(); } ReaderThread::~ReaderThread() { Out = NULL; while (!IsExited()) LgiSleep(1); } int ReaderThread::Main() { bool b = Process->Start(true, false); if (!b) { GString s("Process->Start failed.\n"); Out->Write(s.Get(), s.Length(), ErrSubProcessFailed); return ErrSubProcessFailed; } while (Process->IsRunning()) { if (Out) { char Buf[1024]; ssize_t r = Process->Read(Buf, sizeof(Buf)); if (r > 0) Out->Write(Buf, r); } else { Process->Kill(); return -1; break; } } if (Out) { char Buf[1024]; ssize_t r = Process->Read(Buf, sizeof(Buf)); if (r > 0) Out->Write(Buf, r); } return (int) Process->GetExitValue(); } ///////////////////////////////////////////////////////////////////////////////////////////// void VcFolder::Init(AppPriv *priv) { d = priv; IsCommit = false; IsLogging = false; IsGetCur = false; IsUpdate = false; IsFilesCmd = false; IsWorkingFld = false; CommitListDirty = false; IsUpdatingCounts = false; Unpushed = Unpulled = -1; Type = VcNone; CmdErrors = 0; Expanded(false); Insert(Tmp = new GTreeItem); Tmp->SetText("Loading..."); LgiAssert(d != NULL); } VcFolder::VcFolder(AppPriv *priv, const char *p) { Init(priv); Path = p; } VcFolder::VcFolder(AppPriv *priv, GXmlTag *t) { Init(priv); Serialize(t, false); } VersionCtrl VcFolder::GetType() { if (Type == VcNone) Type = DetectVcs(Path); return Type; } char *VcFolder::GetText(int Col) { switch (Col) { case 0: { if (Cmds.Length()) { Cache = Path; Cache += " (...)"; return Cache; } return Path; } case 1: { CountCache.Printf("%i/%i", Unpulled, Unpushed); CountCache = CountCache.Replace("-1", "--"); return CountCache; } } return NULL; } bool VcFolder::Serialize(GXmlTag *t, bool Write) { if (Write) t->SetContent(Path); else Path = t->GetContent(); return true; } GXmlTag *VcFolder::Save() { GXmlTag *t = new GXmlTag(OPT_Folder); if (t) Serialize(t, true); return t; } const char *VcFolder::GetVcName() { const char *Def = NULL; switch (GetType()) { case VcGit: Def = "git"; break; case VcSvn: Def = "svn"; break; case VcHg: Def = "hg"; break; case VcCvs: Def = "cvs"; break; default: break; } if (!VcCmd) { GString Opt; Opt.Printf("%s-path", Def); GVariant v; if (d->Opts.GetValue(Opt, v)) VcCmd = v.Str(); } if (!VcCmd) VcCmd = Def; return VcCmd; } bool VcFolder::StartCmd(const char *Args, ParseFn Parser, ParseParams *Params, LoggingType Logging) { const char *Exe = GetVcName(); if (!Exe) return false; if (CmdErrors > 2) return false; if (d->Log && Logging != LogSilo) d->Log->Print("%s %s\n", Exe, Args); GAutoPtr Process(new GSubProcess(Exe, Args)); if (!Process) return false; Process->SetInitFolder(Params && Params->AltInitPath ? Params->AltInitPath : Path); GString::Array Ctx; Ctx.SetFixedLength(false); Ctx.Add(Path); Ctx.Add(Exe); Ctx.Add(Args); GAutoPtr c(new Cmd(Ctx, Logging, d->Log)); if (!c) return false; c->PostOp = Parser; c->Params.Reset(Params); c->Rd.Reset(new ReaderThread(Process.Release(), c)); Cmds.Add(c.Release()); Update(); LgiTrace("Cmd: %s %s\n", Exe, Args); return true; } int LogDateCmp(LListItem *a, LListItem *b, NativeInt Data) { VcCommit *A = dynamic_cast(a); VcCommit *B = dynamic_cast(b); if ((A != NULL) ^ (B != NULL)) { // This handles keeping the "working folder" list item at the top return (A != NULL) - (B != NULL); } // Sort the by date from most recent to least return -A->GetTs().Compare(&B->GetTs()); } bool VcFolder::ParseBranches(int Result, GString s, ParseParams *Params) { switch (GetType()) { case VcGit: { GString::Array a = s.SplitDelimit("\r\n"); for (GString *l = NULL; a.Iterate(l); ) { GString n = l->Strip(); if (n(0) == '*') { GString::Array c = n.SplitDelimit(" \t", 1); if (c.Length() > 1) Branches.New() = CurrentBranch = c[1]; else Branches.New() = n; } else Branches.New() = n; } break; } case VcHg: { Branches = s.SplitDelimit("\r\n"); break; } default: { break; } } OnBranchesChange(); return false; } void VcFolder::OnBranchesChange() { GWindow *w = d->Tree->GetWindow(); if (!w) return; DropDownBtn *dd; if (w->GetViewById(IDC_BRANCH_DROPDOWN, dd)) { dd->SetList(IDC_BRANCH, Branches); } if (Branches.Length() > 0) { GViewI *b; if (w->GetViewById(IDC_BRANCH, b)) { if (!ValidStr(b->Name())) - b->Name(Branches.First()); + { + if (CurrentBranch) + b->Name(CurrentBranch); + else + b->Name(Branches.First()); + } } } } void VcFolder::Select(bool b) { if (!b) { GWindow *w = d->Tree->GetWindow(); w->SetCtrlName(IDC_BRANCH, NULL); } GTreeItem::Select(b); if (b) { if (!DirExists(Path)) return; if ((Log.Length() == 0 || CommitListDirty) && !IsLogging) { switch (GetType()) { case VcGit: { IsLogging = StartCmd("rev-list --all --header --timestamp --author-date-order", &VcFolder::ParseRevList); break; } case VcSvn: { if (CommitListDirty) { IsLogging = StartCmd("up", &VcFolder::ParsePull, new ParseParams("log")); break; } // else fall through } default: { IsLogging = StartCmd("log", &VcFolder::ParseLog); } } CommitListDirty = false; } if (Branches.Length() == 0) { switch (GetType()) { case VcGit: StartCmd("branch -a", &VcFolder::ParseBranches); break; case VcSvn: Branches.New() = "trunk"; break; case VcHg: StartCmd("branch", &VcFolder::ParseBranches); break; case VcCvs: break; default: LgiAssert(!"Impl me."); break; } } OnBranchesChange(); /* if (!IsUpdatingCounts && Unpushed < 0) { switch (GetType()) { case VcGit: IsUpdatingCounts = StartCmd("cherry -v", &VcFolder::ParseCounts); break; case VcSvn: IsUpdatingCounts = StartCmd("status -u", &VcFolder::ParseCounts); break; default: LgiAssert(!"Impl me."); break; } } */ char *Ctrl = d->Lst->GetWindow()->GetCtrlName(IDC_FILTER); GString Filter = ValidStr(Ctrl) ? Ctrl : NULL; if (d->CurFolder != this) { d->CurFolder = this; d->Lst->RemoveAll(); } if (!Uncommit) Uncommit.Reset(new UncommitedItem(d)); d->Lst->Insert(Uncommit, 0); int64 CurRev = Atoi(CurrentCommit.Get()); List Ls; for (unsigned i=0; iGetRev()) { switch (GetType()) { case VcSvn: { int64 LogRev = Atoi(Log[i]->GetRev()); if (CurRev >= 0 && CurRev >= LogRev) { CurRev = -1; Log[i]->SetCurrent(true); } else { Log[i]->SetCurrent(false); } break; } default: Log[i]->SetCurrent(!_stricmp(CurrentCommit, Log[i]->GetRev())); break; } } bool Add = !Filter; if (Filter) { const char *s = Log[i]->GetRev(); if (s && strstr(s, Filter) != NULL) Add = true; s = Log[i]->GetAuthor(); if (s && stristr(s, Filter) != NULL) Add = true; s = Log[i]->GetMsg(); if (s && stristr(s, Filter) != NULL) Add = true; } LList *CurOwner = Log[i]->GetList(); if (Add ^ (CurOwner != NULL)) { if (Add) Ls.Insert(Log[i]); else d->Lst->Remove(Log[i]); } } d->Lst->Insert(Ls); // d->Lst->Sort(LogDateCmp); if (GetType() == VcGit) { d->Lst->ColumnAt(0)->Width(40); d->Lst->ColumnAt(1)->Width(270); d->Lst->ColumnAt(2)->Width(240); d->Lst->ColumnAt(3)->Width(130); d->Lst->ColumnAt(4)->Width(400); } else d->Lst->ResizeColumnsToContent(); d->Lst->UpdateAllItems(); if (!CurrentCommit && !IsGetCur) { switch (GetType()) { case VcGit: IsGetCur = StartCmd("rev-parse HEAD", &VcFolder::ParseInfo); break; case VcSvn: IsGetCur = StartCmd("info", &VcFolder::ParseInfo); break; case VcHg: IsGetCur = StartCmd("id -i", &VcFolder::ParseInfo); break; case VcCvs: break; default: LgiAssert(!"Impl me."); break; } } } } int CommitRevCmp(VcCommit **a, VcCommit **b) { int64 arev = Atoi((*a)->GetRev()); int64 brev = Atoi((*b)->GetRev()); int64 diff = (int64)brev - arev; if (diff < 0) return -1; return (diff > 0) ? 1 : 0; } int CommitDateCmp(VcCommit **a, VcCommit **b) { uint64 ats, bts; (*a)->GetTs().Get(ats); (*b)->GetTs().Get(bts); int64 diff = (int64)bts - ats; if (diff < 0) return -1; return (diff > 0) ? 1 : 0; } bool VcFolder::ParseRevList(int Result, GString s, ParseParams *Params) { /* GFile f("C:\\Users\\matthew\\Code\\Lgi\\trunk\\rev-list.txt", O_WRITE); if (f.IsOpen()) { f.SetSize(0); f.Write(s); f.Close(); } */ - LHashTbl, VcCommit*> Map; - for (VcCommit **pc = NULL; Log.Iterate(pc); ) - Map.Add((*pc)->GetRev(), *pc); + /* + LHashTbl, int> Map(0, -1); + for (unsigned i=0; iGetRev(), i); + */ + Log.DeleteObjects(); int Skipped = 0, Errors = 0; switch (GetType()) { case VcGit: { GString::Array Commits; Commits.SetFixedLength(false); // Split on the NULL chars... char *c = s.Get(); char *e = c + s.Length(); while (c < e) { char *nul = c; while (nul < e && *nul) nul++; if (nul <= c) break; Commits.New().Set(c, nul-c); if (nul >= e) break; c = nul + 1; } for (auto Commit: Commits) { GAutoPtr Rev(new VcCommit(d, this)); if (Rev->GitParse(Commit, true)) { - if (!Map.Find(Rev->GetRev())) - Log.Add(Rev.Release()); - else - Skipped++; + Log.Add(Rev.Release()); } else { LgiTrace("%s:%i - Failed:\n%s\n\n", _FL, Commit.Get()); Errors++; } } // Log.Sort(CommitDateCmp); LinkParents(); break; } default: LgiAssert(!"Impl me."); break; } + IsLogging = false; return true; } bool VcFolder::ParseLog(int Result, GString s, ParseParams *Params) { LHashTbl, VcCommit*> Map; for (VcCommit **pc = NULL; Log.Iterate(pc); ) Map.Add((*pc)->GetRev(), *pc); int Skipped = 0, Errors = 0; switch (GetType()) { case VcGit: { GString::Array c; c.SetFixedLength(false); char *prev = s.Get(); for (char *i = s.Get(); *i; ) { if (!strnicmp(i, "commit ", 7)) { if (i > prev) { c.New().Set(prev, i - prev); // LgiTrace("commit=%i\n", (int)(i - prev)); } prev = i; } while (*i) { if (*i++ == '\n') break; } } for (unsigned i=0; i Rev(new VcCommit(d, this)); if (Rev->GitParse(c[i], false)) { if (!Map.Find(Rev->GetRev())) Log.Add(Rev.Release()); else Skipped++; } else { LgiTrace("%s:%i - Failed:\n%s\n\n", _FL, c[i].Get()); Errors++; } } Log.Sort(CommitDateCmp); break; } case VcSvn: { GString::Array c = s.Split("------------------------------------------------------------------------"); for (unsigned i=0; i Rev(new VcCommit(d, this)); GString Raw = c[i].Strip(); if (Rev->SvnParse(Raw)) { if (!Map.Find(Rev->GetRev())) Log.Add(Rev.Release()); else Skipped++; } else if (Raw) { LgiTrace("%s:%i - Failed:\n%s\n\n", _FL, Raw.Get()); Errors++; } } Log.Sort(CommitRevCmp); break; } case VcHg: { GString::Array c = s.Split("\n\n"); for (GString *Commit = NULL; c.Iterate(Commit); ) { GAutoPtr Rev(new VcCommit(d, this)); if (Rev->HgParse(*Commit)) { if (!Map.Find(Rev->GetRev())) Log.Add(Rev.Release()); } } break; } case VcCvs: { LHashTbl, VcCommit*> Map; GString::Array c = s.Split("============================================================================="); for (GString *Commit = NULL; c.Iterate(Commit);) { if (Commit->Strip().Length()) { GString Head, File; GString::Array Versions = Commit->Split("----------------------------"); GString::Array Lines = Versions[0].SplitDelimit("\r\n"); for (GString *Line = NULL; Lines.Iterate(Line);) { GString::Array p = Line->Split(":", 1); if (p.Length() == 2) { // LgiTrace("Line: %s\n", Line->Get()); GString Var = p[0].Strip().Lower(); GString Val = p[1].Strip(); if (Var.Equals("branch")) { if (Val.Length()) Branches.Add(Val); } else if (Var.Equals("head")) { Head = Val; } else if (Var.Equals("rcs file")) { GString::Array f = Val.SplitDelimit(","); File = f.First(); } } } // LgiTrace("%s\n", Commit->Get()); for (unsigned i=1; i= 3) { GString Ver = Lines[0].Split(" ").Last(); GString::Array a = Lines[1].SplitDelimit(";"); GString Date = a[0].Split(":", 1).Last().Strip(); GString Author = a[1].Split(":", 1).Last().Strip(); GString Id = a[2].Split(":", 1).Last().Strip(); GString Msg = Lines[2]; LDateTime Dt; if (Dt.Parse(Date)) { uint64 Ts; if (Dt.Get(Ts)) { VcCommit *Cc = Map.Find(Ts); if (!Cc) { Map.Add(Ts, Cc = new VcCommit(d, this)); Log.Add(Cc); Cc->CvsParse(Dt, Author, Msg); } Cc->Files.Add(File.Get()); } else LgiAssert(!"NO ts for date."); } else LgiAssert(!"Date parsing failed."); } } } } break; } default: LgiAssert(!"Impl me."); break; } // LgiTrace("%s:%i - ParseLog: Skip=%i, Error=%i\n", _FL, Skipped, Errors); IsLogging = false; return true; } void VcFolder::LinkParents() { LHashTbl,VcCommit*> Map; // Index all the commits int i = 0; for (auto c:Log) { c->Idx = i++; c->NodeIdx = -1; Map.Add(c->GetRev(), c); } // Create all the edges... for (auto c:Log) { auto *Par = c->GetParents(); for (auto &pRev : *Par) { auto *p = Map.Find(pRev); if (p) new VcEdge(p, c); else LgiAssert(0); } } // Map the edges to positions typedef GArray EdgeArr; GArray Active; for (auto c:Log) { for (unsigned i=0; c->NodeIdx<0 && iParent == c) { c->NodeIdx = i; break; } } } // Add starting edges to active set for (auto e:c->Edges) { if (e->Child == c) { if (c->NodeIdx < 0) c->NodeIdx = (int)Active.Length(); e->Idx = c->NodeIdx; c->Pos.Add(e, e->Idx); Active[e->Idx].Add(e); } } // Now for all active edges... assign positions for (unsigned i=0; iChild || c == e->Parent) { LgiAssert(c->NodeIdx >= 0); c->Pos.Add(e, c->NodeIdx); } else { // May need to untangle edges with different parents here bool Diff = false; for (auto edge: Edges) { if (edge != e && edge->Child != c && edge->Parent != e->Parent) { Diff = true; break; } } if (Diff) { int NewIndex = -1; // Look through existing indexes for a parent match for (unsigned ii=0; iiParent? bool Match = true; for (auto ee:Active[ii]) { if (ee->Parent != e->Parent) { Match = false; break; } } if (Match) NewIndex = ii; } if (NewIndex < 0) // Create new index for this parent NewIndex = (int)Active.Length(); Edges.Delete(e); Active[NewIndex].Add(e); e->Idx = NewIndex; c->Pos.Add(e, NewIndex); n--; } else { LgiAssert(e->Idx == i); c->Pos.Add(e, i); } } } } // Process terminating edges for (auto e:c->Edges) { if (e->Parent == c) { if (e->Idx < 0) { // This happens with out of order commits..? continue; } int i = e->Idx; if (c->NodeIdx < 0) c->NodeIdx = i; LgiAssert(Active[i].HasItem(e)); Active[i].Delete(e); } } // Collapse any empty active columns for (unsigned i=0; iIdx > 0); edge->Idx--; c->Pos.Add(edge, edge->Idx); } } i--; } } } // Find all the "heads", i.e. a commit without any children GCombo *Heads; if (d->Files->GetWindow()->GetViewById(IDC_HEADS, Heads)) { Heads->Empty(); for (auto c:Log) { bool Has = false; for (auto e:c->Edges) { if (e->Parent == c) { Has = true; break; } } if (!Has) Heads->Insert(c->GetRev()); } Heads->SendNotify(GNotifyTableLayout_Refresh); } } VcFile *VcFolder::FindFile(const char *Path) { if (!Path) return NULL; GArray Files; if (d->Files->GetAll(Files)) { GString p = Path; p = p.Replace(DIR_STR, "/"); for (auto f : Files) { auto Fn = f->GetFileName(); if (p.Equals(Fn)) { return f; } } } return NULL; } void VcFolder::OnCmdError(GString Output, const char *Msg) { if (!CmdErrors) { d->Log->Write(Output, Output.Length()); GString::Array a = GetProgramsInPath(GetVcName()); d->Log->Print("'%s' executables in the path:\n", GetVcName()); for (auto Bin : a) d->Log->Print(" %s\n", Bin.Get()); } CmdErrors++; d->Tabs->Value(1); Color(GColour::Red); } void VcFolder::ClearError() { Color(GCss::ColorInherit); } bool VcFolder::ParseInfo(int Result, GString s, ParseParams *Params) { switch (GetType()) { case VcGit: case VcHg: { CurrentCommit = s.Strip(); break; } case VcSvn: { if (s.Find("client is too old") >= 0) { OnCmdError(s, "Client too old"); break; } GString::Array c = s.Split("\n"); for (unsigned i=0; iIsWorking = true; ParseStatus(Result, s, Params); } else ParseDiffs(s, NULL, true); IsWorkingFld = false; d->Files->ResizeColumnsToContent(); if (GetType() == VcSvn) { Unpushed = d->Files->Length() > 0 ? 1 : 0; Update(); } return false; } bool VcFolder::ParseDiff(int Result, GString s, ParseParams *Params) { ParseDiffs(s, NULL, true); return false; } void VcFolder::Diff(VcFile *file) { switch (GetType()) { case VcSvn: case VcGit: case VcHg: { GString a; a.Printf("diff \"%s\"", file->GetFileName()); StartCmd(a, &VcFolder::ParseDiff); break; } default: LgiAssert(!"Impl me."); break; } } bool VcFolder::ParseDiffs(GString s, GString Rev, bool IsWorking) { LgiAssert(IsWorking || Rev.Get() != NULL); switch (GetType()) { case VcGit: { GString::Array a = s.Split("\n"); GString Diff; VcFile *f = NULL; for (unsigned i=0; iSetDiff(Diff); Diff.Empty(); auto Bits = a[i].SplitDelimit(); GString Fn, State = "M"; if (Bits[1].Equals("--cc")) { Fn = Bits.Last(); State = "C"; } else Fn = Bits.Last()(2,-1); LgiTrace("%s\n", a[i].Get()); f = FindFile(Fn); if (!f) f = new VcFile(d, this, Rev, IsWorking); f->SetText(State, COL_STATE); f->SetText(Fn.Replace("\\","/"), COL_FILENAME); f->GetStatus(); d->Files->Insert(f); } else if (!_strnicmp(Ln, "new file", 8)) { if (f) f->SetText("A", COL_STATE); } else if (!_strnicmp(Ln, "index", 5) || !_strnicmp(Ln, "commit", 6) || !_strnicmp(Ln, "Author:", 7) || !_strnicmp(Ln, "Date:", 5) || !_strnicmp(Ln, "+++", 3) || !_strnicmp(Ln, "---", 3)) { // Ignore } else { if (Diff) Diff += "\n"; Diff += a[i]; } } if (f && Diff) { f->SetDiff(Diff); Diff.Empty(); } break; } case VcHg: { GString::Array a = s.Split("\n"); GString Diff; VcFile *f = NULL; for (unsigned i=0; iSetDiff(Diff); Diff.Empty(); GString Fn = a[i].Split(" ").Last(); f = new VcFile(d, this, Rev, IsWorking); f->SetText(Fn.Replace("\\","/"), COL_FILENAME); d->Files->Insert(f); } else if (!_strnicmp(Ln, "index", 5) || !_strnicmp(Ln, "commit", 6) || !_strnicmp(Ln, "Author:", 7) || !_strnicmp(Ln, "Date:", 5) || !_strnicmp(Ln, "+++", 3) || !_strnicmp(Ln, "---", 3)) { // Ignore } else { if (Diff) Diff += "\n"; Diff += a[i]; } } if (f && Diff) { f->SetDiff(Diff); Diff.Empty(); } break; } case VcSvn: { GString::Array a = s.Replace("\r").Split("\n"); GString Diff; VcFile *f = NULL; bool InPreamble = false; bool InDiff = false; for (unsigned i=0; iSetDiff(Diff); f->Select(false); } Diff.Empty(); InDiff = false; InPreamble = false; GString Fn = a[i].Split(":", 1).Last().Strip(); f = FindFile(Fn); if (!f) f = new VcFile(d, this, Rev, IsWorking); f->SetText(Fn.Replace("\\","/"), COL_FILENAME); f->SetText("M", COL_STATE); f->GetStatus(); d->Files->Insert(f); } else if (!_strnicmp(Ln, "------", 6)) { InPreamble = !InPreamble; } else if (!_strnicmp(Ln, "======", 6)) { InPreamble = false; InDiff = true; } else if (InDiff) { if (!strncmp(Ln, "--- ", 4) || !strncmp(Ln, "+++ ", 4)) { } else { if (Diff) Diff += "\n"; Diff += a[i]; } } } if (f && Diff) { f->SetDiff(Diff); Diff.Empty(); } break; } case VcCvs: { break; } default: { LgiAssert(!"Impl me."); break; } } return true; } bool VcFolder::ParseFiles(int Result, GString s, ParseParams *Params) { d->ClearFiles(); ParseDiffs(s, Params->Str, false); IsFilesCmd = false; d->Files->ResizeColumnsToContent(); return false; } void VcFolder::OnPulse() { bool Reselect = false, CmdsChanged = false; static bool Processing = false; if (!Processing) { Processing = true; // Lock out processing, if it puts up a dialog or something... // bad things happen if we try and re-process something. for (unsigned i=0; iRd->IsExited()) { GString s = c->GetBuf(); int Result = c->Rd->ExitCode(); if (Result == ErrSubProcessFailed) { if (!CmdErrors) d->Log->Print("Error: Can't run '%s'\n", GetVcName()); CmdErrors++; } if (c->PostOp) Reselect |= CALL_MEMBER_FN(*this, c->PostOp)(Result, s, c->Params); Cmds.DeleteAt(i--, true); delete c; CmdsChanged = true; } } Processing = false; } if (Reselect) { if (GTreeItem::Select()) Select(true); } if (CmdsChanged) Update(); } void VcFolder::OnRemove() { GXmlTag *t = d->Opts.LockTag(NULL, _FL); if (t) { for (GXmlTag *c = t->Children.First(); c; c = t->Children.Next()) { if (c->IsTag(OPT_Folder) && c->GetContent() && !_stricmp(c->GetContent(), Path)) { c->RemoveTag(); delete c; break; } } d->Opts.Unlock(); } } void VcFolder::OnMouseClick(GMouse &m) { if (m.IsContextMenu()) { GSubMenu s; s.AppendItem("Browse To", IDM_BROWSE_FOLDER); s.AppendItem( #ifdef WINDOWS "Command Prompt At", #else "Terminal At", #endif IDM_TERMINAL); s.AppendItem("Clean", IDM_CLEAN); s.AppendSeparator(); s.AppendItem("Remove", IDM_REMOVE); int Cmd = s.Float(GetTree(), m); switch (Cmd) { case IDM_BROWSE_FOLDER: { LgiBrowseToFile(Path); break; } case IDM_TERMINAL: { #if defined(MAC) LgiExecute("/Applications/Utilities/Terminal.app/Contents/MacOS/Terminal", Path); #elif defined(WINDOWS) TCHAR w[MAX_PATH]; auto r = GetWindowsDirectory(w, CountOf(w)); if (r > 0) { GFile::Path p = GString(w); p += "system32\\cmd.exe"; FileDev->SetCurrentFolder(Path); LgiExecute(p); } #elif defined(LINUX) // #error "Impl me." #endif break; } case IDM_CLEAN: { Clean(); break; } case IDM_REMOVE: { OnRemove(); delete this; break; } default: break; } } } void VcFolder::OnUpdate(const char *Rev) { if (!Rev) return; if (!IsUpdate) { GString Args; NewRev = Rev; switch (GetType()) { case VcGit: Args.Printf("checkout %s", Rev); IsUpdate = StartCmd(Args, &VcFolder::ParseUpdate, NULL, LogNormal); break; case VcSvn: Args.Printf("up -r %s", Rev); IsUpdate = StartCmd(Args, &VcFolder::ParseUpdate, NULL, LogNormal); break; default: { LgiAssert(!"Impl me."); break; } } } } /////////////////////////////////////////////////////////////////////////////////////// int FolderCompare(GTreeItem *a, GTreeItem *b, NativeInt UserData) { VcLeaf *A = dynamic_cast(a); VcLeaf *B = dynamic_cast(b); if (!A || !B) return 0; return A->Compare(B); } void VcFolder::ReadDir(GTreeItem *Parent, const char *Path) { // Read child items GDirectory Dir; for (int b = Dir.First(Path); b; b = Dir.Next()) { if (Dir.IsDir()) { if (Dir.GetName()[0] != '.') { new VcLeaf(this, Parent, Path, Dir.GetName(), true); } } else if (!Dir.IsHidden()) { char *Ext = LgiGetExtension(Dir.GetName()); if (!Ext) continue; if (!stricmp(Ext, "c") || !stricmp(Ext, "cpp") || !stricmp(Ext, "h")) { new VcLeaf(this, Parent, Path, Dir.GetName(), false); } } } Parent->SortChildren(FolderCompare); } void VcFolder::OnExpand(bool b) { if (Tmp && b) { Tmp->Remove(); DeleteObj(Tmp); ReadDir(this, Path); } } void VcFolder::OnPaint(ItemPaintCtx &Ctx) { auto c = Color(); if (c.IsValid()) Ctx.Fore = c; c = BackgroundColor(); if (c.IsValid() && !GTreeItem::Select()) Ctx.Back = c; GTreeItem::OnPaint(Ctx); } void VcFolder::ListCommit(VcCommit *c) { if (!IsFilesCmd) { GString Args; switch (GetType()) { case VcGit: // Args.Printf("show --oneline --name-only %s", Rev); Args.Printf("show %s", c->GetRev()); IsFilesCmd = StartCmd(Args, &VcFolder::ParseFiles, new ParseParams(c->GetRev())); break; case VcSvn: Args.Printf("log --verbose --diff -r %s", c->GetRev()); IsFilesCmd = StartCmd(Args, &VcFolder::ParseFiles, new ParseParams(c->GetRev())); break; case VcCvs: { d->ClearFiles(); for (unsigned i=0; iFiles.Length(); i++) { VcFile *f = new VcFile(d, this, c->GetRev(), false); if (f) { f->SetText(c->Files[i], COL_FILENAME); d->Files->Insert(f); } } d->Files->ResizeColumnsToContent(); break; } default: LgiAssert(!"Impl me."); break; } if (IsFilesCmd) d->ClearFiles(); } } GString ConvertUPlus(GString s) { GArray c; GUtf8Ptr p(s); int32 ch; while ((ch = p)) { if (ch == '{') { auto n = p.GetPtr(); if (n[1] == 'U' && n[2] == '+') { // Convert unicode code point p += 3; ch = (int32)htoi(p.GetPtr()); c.Add(ch); while ((ch = p) != '}') p++; } else c.Add(ch); } else c.Add(ch); p++; } c.Add(0); #ifdef LINUX return GString((char16*)c.AddressOf()); #else return GString(c.AddressOf()); #endif } bool VcFolder::ParseStatus(int Result, GString s, ParseParams *Params) { bool ShowUntracked = d->Files->GetWindow()->GetCtrlValue(IDC_UNTRACKED) != 0; bool IsWorking = Params ? Params->IsWorking : false; List Ins; switch (GetType()) { case VcCvs: { GString::Array a = s.Split("==================================================================="); for (auto i : a) { GString::Array Lines = i.SplitDelimit("\r\n"); GString f = Lines[0].Strip(); if (f.Find("File:") == 0) { GString::Array Parts = f.SplitDelimit("\t"); GString File = Parts[0].Split(": ").Last(); GString Status = Parts[1].Split(": ").Last(); GString WorkingRev; for (auto l : Lines) { GString::Array p = l.Strip().Split(":", 1); if (p.Length() > 1 && p[0].Strip().Equals("Working revision")) { WorkingRev = p[1].Strip(); } } VcFile *f = new VcFile(d, this, WorkingRev, IsWorking); f->SetText(Status, COL_STATE); f->SetText(File, COL_FILENAME); Ins.Insert(f); } else if (f(0) == '?' && ShowUntracked) { GString File = f(2, -1); VcFile *f = new VcFile(d, this, NULL, IsWorking); f->SetText("?", COL_STATE); f->SetText(File, COL_FILENAME); Ins.Insert(f); } } break; } case VcGit: { GString::Array Lines = s.SplitDelimit("\r\n"); int Fmt = ToolVersion[VcGit] >= Ver2Int("2.8.0") ? 2 : 1; for (auto Ln : Lines) { char Type = Ln(0); if (Ln.Lower().Find("error:") >= 0) { } else if (Ln.Find("usage: git") >= 0) { // It's probably complaining about the --porcelain=2 parameter OnCmdError(s, "Args error"); } else if (Type != '?') { VcFile *f = NULL; if (Fmt == 2) { GString::Array p = Ln.SplitDelimit(" ", 8); if (p.Length() < 7) d->Log->Print("%s:%i - Error: not enough tokens: '%s'\n", _FL, Ln.Get()); else { f = new VcFile(d, this, p[6], IsWorking); f->SetText(p[1].Strip("."), COL_STATE); f->SetText(p.Last(), COL_FILENAME); } } else if (Fmt == 1) { GString::Array p = Ln.SplitDelimit(" "); f = new VcFile(d, this, NULL, IsWorking); f->SetText(p[0], COL_STATE); f->SetText(p.Last(), COL_FILENAME); } if (f) Ins.Insert(f); } else if (ShowUntracked) { VcFile *f = new VcFile(d, this, NULL, IsWorking); f->SetText("?", COL_STATE); f->SetText(Ln(2,-1), COL_FILENAME); Ins.Insert(f); } } break; } case VcHg: case VcSvn: { GString::Array Lines = s.SplitDelimit("\r\n"); for (auto Ln : Lines) { char Type = Ln(0); if (Ln.Lower().Find("error:") >= 0) { } else if (Ln.Find("client is too old") >= 0) { OnCmdError(s, "Client too old."); return false; } else if (Strchr(" \t", Type) || Ln.Find("Summary of conflicts") >= 0) { // Ignore } else if (Type != '?') { GString::Array p = Ln.SplitDelimit(" ", 1); if (p.Length() == 2) { GString File; if (GetType() == VcSvn) File = ConvertUPlus(p.Last()); else File = p.Last(); VcFile *f = new VcFile(d, this, NULL, IsWorking); f->SetText(p[0], COL_STATE); f->SetText(File.Replace("\\","/"), COL_FILENAME); f->GetStatus(); Ins.Insert(f); } else LgiAssert(!"What happen?"); } else if (ShowUntracked) { VcFile *f = new VcFile(d, this, NULL, IsWorking); f->SetText("?", COL_STATE); f->SetText(Ln(2,-1), COL_FILENAME); Ins.Insert(f); } } break; } default: { LgiAssert(!"Impl me."); break; } } Unpushed = Ins.Length() > 0; Update(); if (GTreeItem::Select()) { d->Files->Insert(Ins); d->Files->ResizeColumnsToContent(); } else { Ins.DeleteObjects(); } return false; // Don't refresh list } void VcFolder::FolderStatus(const char *Path, VcLeaf *Notify) { if (GTreeItem::Select()) d->ClearFiles(); GString Arg; switch (GetType()) { case VcSvn: case VcHg: Arg = "status"; break; case VcCvs: Arg = "status -l"; break; case VcGit: if (!ToolVersion[VcGit]) LgiAssert(!"Where is the version?"); // What version did =2 become available? It's definately not in v2.5.4 // Not in v2.7.4 either... if (ToolVersion[VcGit] >= Ver2Int("2.8.0")) Arg = "status --porcelain=2"; else Arg = "status --porcelain"; break; default: return; } ParseParams *p = new ParseParams; if (Path && Notify) { p->AltInitPath = Path; p->Leaf = Notify; } else { p->IsWorking = true; } StartCmd(Arg, &VcFolder::ParseStatus, p); } void VcFolder::ListWorkingFolder() { if (!IsWorkingFld) { d->ClearFiles(); GString Arg; switch (GetType()) { case VcCvs: Arg = "-q diff --brief"; break; case VcSvn: Arg = "status"; break; case VcGit: StartCmd("diff --staged", &VcFolder::ParseWorking); Arg = "diff --diff-filter=ACDMRTU"; // return FolderStatus(); break; default: Arg ="diff"; break; } IsWorkingFld = StartCmd(Arg, &VcFolder::ParseWorking); } } void VcFolder::GitAdd() { if (!PostAdd) return; GString Args; if (PostAdd->Files.Length() == 0) { GString m(PostAdd->Msg); m = m.Replace("\"", "\\\""); Args.Printf("commit -m \"%s\"", m.Get()); IsCommit = StartCmd(Args, &VcFolder::ParseCommit, PostAdd->Param, LogNormal); PostAdd.Reset(); } else { GString Last = PostAdd->Files.Last(); Args.Printf("add \"%s\"", Last.Replace("\"", "\\\"").Replace("/", DIR_STR).Get()); PostAdd->Files.PopLast(); StartCmd(Args, &VcFolder::ParseGitAdd, NULL, LogNormal); } } bool VcFolder::ParseGitAdd(int Result, GString s, ParseParams *Params) { GitAdd(); return false; } bool VcFolder::ParseCommit(int Result, GString s, ParseParams *Params) { if (GTreeItem::Select()) Select(true); CommitListDirty = Result == 0; CurrentCommit.Empty(); IsCommit = false; if (Result) { switch (GetType()) { case VcGit: { if (s.Find("Please tell me who you are") >= 0) { { GInput i(GetTree(), "", "Git user name:", AppName); if (i.DoModal()) { GString Args; Args.Printf("config --global user.name \"%s\"", i.GetStr().Get()); StartCmd(Args); } } { GInput i(GetTree(), "", "Git user email:", AppName); if (i.DoModal()) { GString Args; Args.Printf("config --global user.email \"%s\"", i.GetStr().Get()); StartCmd(Args); } } } break; } default: break; } return false; } if (Result == 0 && GTreeItem::Select()) { d->ClearFiles(); GWindow *w = d->Diff ? d->Diff->GetWindow() : NULL; if (w) w->SetCtrlName(IDC_MSG, NULL); } switch (GetType()) { case VcGit: { Unpushed++; + CommitListDirty = true; Update(); if (Params && Params->Str.Find("Push") >= 0) Push(); break; } case VcSvn: { CurrentCommit.Empty(); CommitListDirty = true; GetTree()->SendNotify(LvcCommandEnd); if (!Result) { Unpushed = 0; Update(); } break; } default: { LgiAssert(!"Impl me."); break; } } return true; } void VcFolder::Commit(const char *Msg, const char *Branch, bool AndPush) { VcFile *f = NULL; GArray Add; bool Partial = false; while (d->Files->Iterate(f)) { int c = f->Checked(); if (c > 0) Add.Add(f); else Partial = true; } if (CurrentBranch && Branch && !CurrentBranch.Equals(Branch)) { int Response = LgiMsg(GetTree(), "Do you want to start a new branch?", AppName, MB_YESNO); if (Response != IDYES) return; } if (!IsCommit) { GString Args; ParseParams *Param = AndPush ? new ParseParams("Push") : NULL; switch (GetType()) { case VcGit: { if (Add.Length() == 0) { break; } else if (Partial) { if (PostAdd.Reset(new GitCommit)) { PostAdd->Files.SetFixedLength(false); for (auto f : Add) PostAdd->Files.Add(f->GetFileName()); PostAdd->Msg = Msg; PostAdd->Branch = Branch; PostAdd->Param = Param; GitAdd(); } } else { GString m(Msg); m = m.Replace("\"", "\\\""); Args.Printf("commit -am \"%s\"", m.Get()); IsCommit = StartCmd(Args, &VcFolder::ParseCommit, Param, LogNormal); } break; } case VcSvn: { GString::Array a; a.New().Printf("commit -m \"%s\"", Msg); for (VcFile **pf = NULL; Add.Iterate(pf); ) { GString s = (*pf)->GetFileName(); if (s.Find(" ") >= 0) a.New().Printf("\"%s\"", s.Get()); else a.New() = s; } Args = GString(" ").Join(a); IsCommit = StartCmd(Args, &VcFolder::ParseCommit, NULL, LogNormal); if (d->Tabs && IsCommit) { d->Tabs->Value(1); GetTree()->SendNotify(LvcCommandStart); } break; } default: { LgiAssert(!"Impl me."); break; } } } } void VcFolder::Push() { GString Args; bool Working = false; switch (GetType()) { case VcGit: Working = StartCmd("push", &VcFolder::ParsePush, NULL, LogNormal); break; case VcSvn: // Nothing to do here.. the commit pushed the data already break; default: LgiAssert(!"Impl me."); break; } if (d->Tabs && Working) { d->Tabs->Value(1); GetTree()->SendNotify(LvcCommandStart); } } bool VcFolder::ParsePush(int Result, GString s, ParseParams *Params) { bool Status = false; if (Result) { OnCmdError(s, "Push failed."); } else { ClearError(); switch (GetType()) { case VcGit: break; case VcSvn: break; default: break; } Unpushed = 0; Update(); Status = true; } GetTree()->SendNotify(LvcCommandEnd); return Status; // no reselect } void VcFolder::Pull(LoggingType Logging) { GString Args; bool Status = false; switch (GetType()) { case VcHg: case VcGit: Status = StartCmd("pull", &VcFolder::ParsePull, NULL, Logging); break; case VcSvn: Status = StartCmd("up", &VcFolder::ParsePull, NULL, Logging); break; default: LgiAssert(!"Impl me."); break; } if (d->Tabs && Status) { d->Tabs->Value(1); GetTree()->SendNotify(LvcCommandStart); } } bool VcFolder::ParsePull(int Result, GString s, ParseParams *Params) { if (Result) { OnCmdError(s, "Pull failed."); return false; } else ClearError(); switch (GetType()) { case VcGit: case VcHg: { // Git does a merge by default, so the current commit changes... CurrentCommit.Empty(); break; } case VcSvn: { // Svn also does a merge by default and can update our current position... CurrentCommit.Empty(); GString::Array a = s.SplitDelimit("\r\n"); for (GString *Ln = NULL; a.Iterate(Ln); ) { if (Ln->Find("At revision") >= 0) { GString::Array p = Ln->SplitDelimit(" ."); CurrentCommit = p.Last(); break; } else if (Ln->Find("svn cleanup") >= 0) { OnCmdError(s, "Needs cleanup"); break; } } if (Params && Params->Str.Equals("log")) { IsLogging = StartCmd("log", &VcFolder::ParseLog); return false; } break; } default: break; } GetTree()->SendNotify(LvcCommandEnd); CommitListDirty = true; return true; // Yes - reselect and update } void VcFolder::MergeToLocal(GString Rev) { switch (GetType()) { case VcGit: { GString Args; Args.Printf("merge -m \"Merge with %s\" %s", Rev.Get(), Rev.Get()); StartCmd(Args, &VcFolder::ParseMerge, NULL, LogNormal); break; } default: LgiMsg(GetTree(), "Not implemented.", AppName); break; } } bool VcFolder::ParseMerge(int Result, GString s, ParseParams *Params) { switch (GetType()) { case VcGit: if (Result == 0) CommitListDirty = true; break; default: LgiAssert(!"Impl me."); break; } return true; } void VcFolder::Clean() { switch (GetType()) { case VcSvn: StartCmd("cleanup", &VcFolder::ParseClean, NULL, LogNormal); break; default: LgiMsg(GetTree(), "Not implemented.", AppName); break; } } bool VcFolder::ParseClean(int Result, GString s, ParseParams *Params) { switch (GetType()) { case VcSvn: if (Result == 0) Color(ColorInherit); break; default: LgiAssert(!"Impl me."); break; } return false; } void VcFolder::GetVersion() { auto t = GetType(); switch (t) { case VcGit: case VcSvn: case VcHg: case VcCvs: StartCmd("--version", &VcFolder::ParseVersion, NULL, LogNormal); break; default: OnCmdError(NULL, "No version control found."); break; } } bool VcFolder::ParseVersion(int Result, GString s, ParseParams *Params) { auto p = s.SplitDelimit(); switch (GetType()) { case VcGit: { if (p.Length() > 2) { ToolVersion[GetType()] = Ver2Int(p[2]); printf("Git version: %s\n", p[2].Get()); } else LgiAssert(0); break; } case VcSvn: { if (p.Length() > 2) { ToolVersion[GetType()] = Ver2Int(p[2]); printf("Svn version: %s\n", p[2].Get()); } else LgiAssert(0); break; } case VcHg: { if (p.Length() >= 5) { auto Ver = p[4].Strip("()"); ToolVersion[GetType()] = Ver2Int(Ver); printf("Hg version: %s\n", Ver.Get()); } break; } case VcCvs: { #ifdef _DEBUG for (auto i : p) printf("i='%s'\n", i.Get()); #endif LgiAssert(!"Impl me."); break; } default: break; } return false; } bool VcFolder::ParseAddFile(int Result, GString s, ParseParams *Params) { switch (GetType()) { case VcCvs: { break; } default: break; } return false; } bool VcFolder::AddFile(const char *Path, bool AsBinary) { if (!Path) return false; switch (GetType()) { case VcCvs: { GString a; a.Printf("add%s \"%s\"", AsBinary ? " -kb" : "", Path); return StartCmd(a, &VcFolder::ParseAddFile); break; } default: { LgiAssert(!"Impl me."); break; } } return false; } bool VcFolder::ParseRevert(int Result, GString s, ParseParams *Params) { ListWorkingFolder(); return false; } bool VcFolder::Revert(const char *Path, const char *Revision) { if (!Path) return false; switch (GetType()) { case VcGit: { GString a; a.Printf("checkout \"%s\"", Path); return StartCmd(a, &VcFolder::ParseRevert); break; } case VcSvn: { GString a; a.Printf("revert \"%s\"", Path); return StartCmd(a, &VcFolder::ParseRevert); break; } default: { LgiAssert(!"Impl me."); break; } } return false; } bool VcFolder::ParseResolve(int Result, GString s, ParseParams *Params) { switch (GetType()) { case VcGit: { break; } case VcSvn: case VcHg: case VcCvs: default: { LgiAssert(!"Impl me."); break; } } return true; } bool VcFolder::Resolve(const char *Path) { if (!Path) return false; switch (GetType()) { case VcGit: { GString a; a.Printf("add \"%s\"", Path); return StartCmd(a, &VcFolder::ParseResolve, new ParseParams(Path)); } case VcSvn: case VcHg: case VcCvs: default: { LgiAssert(!"Impl me."); break; } } return false; } bool VcFolder::ParseBlame(int Result, GString s, ParseParams *Params) { new BlameUi(d, GetType(), s); return false; } bool VcFolder::Blame(const char *Path) { if (!Path) return false; switch (GetType()) { case VcGit: { GString a; a.Printf("blame \"%s\"", Path); return StartCmd(a, &VcFolder::ParseBlame); break; } case VcHg: { GString a; a.Printf("annotate -un \"%s\"", Path); return StartCmd(a, &VcFolder::ParseBlame); break; } case VcSvn: { GString a; a.Printf("blame \"%s\"", Path); return StartCmd(a, &VcFolder::ParseBlame); break; } default: { LgiAssert(!"Impl me."); break; } } return true; } bool VcFolder::SaveFileAs(const char *Path, const char *Revision) { if (!Path || !Revision) return false; return true; } bool VcFolder::ParseSaveAs(int Result, GString s, ParseParams *Params) { return false; } bool VcFolder::ParseCounts(int Result, GString s, ParseParams *Params) { switch (GetType()) { case VcGit: { Unpushed = (int) s.Strip().Split("\n").Length(); break; } case VcSvn: { int64 ServerRev = 0; bool HasUpdate = false; GString::Array c = s.Split("\n"); for (unsigned i=0; i 1 && a[0].Equals("Status")) ServerRev = a.Last().Int(); else if (a[0].Equals("*")) HasUpdate = true; } if (ServerRev > 0 && HasUpdate) { int64 CurRev = CurrentCommit.Int(); Unpulled = (int) (ServerRev - CurRev); } else Unpulled = 0; Update(); break; } default: { LgiAssert(!"Impl me."); break; } } IsUpdatingCounts = false; Update(); return false; // No re-select } void VcFolder::SetEol(const char *Path, int Type) { if (!Path) return; switch (Type) { case IDM_EOL_LF: { ConvertEol(Path, false); break; } case IDM_EOL_CRLF: { ConvertEol(Path, true); break; } case IDM_EOL_AUTO: { #ifdef WINDOWS ConvertEol(Path, true); #else ConvertEol(Path, false); #endif break; } } } void VcFolder::UncommitedItem::Select(bool b) { LListItem::Select(b); if (b) { GTreeItem *i = d->Tree->Selection(); VcFolder *f = dynamic_cast(i); if (f) f->ListWorkingFolder(); if (d->Msg) { d->Msg->Name(NULL); GWindow *w = d->Msg->GetWindow(); if (w) { w->SetCtrlEnabled(IDC_COMMIT, true); w->SetCtrlEnabled(IDC_COMMIT_AND_PUSH, true); } } } } void VcFolder::UncommitedItem::OnPaint(GItem::ItemPaintCtx &Ctx) { GFont *f = GetList()->GetFont(); f->Transparent(false); f->Colour(Ctx.Fore, Ctx.Back); GDisplayString ds(f, "(working folder)"); ds.Draw(Ctx.pDC, Ctx.x1 + ((Ctx.X() - ds.X()) / 2), Ctx.y1 + ((Ctx.Y() - ds.Y()) / 2), &Ctx); } ////////////////////////////////////////////////////////////////////////////////////////// VcLeaf::VcLeaf(VcFolder *parent, GTreeItem *Item, GString path, GString leaf, bool folder) { Parent = parent; d = Parent->GetPriv(); Path = path; Leaf = leaf; Folder = folder; Tmp = NULL; Item->Insert(this); if (Folder) { Insert(Tmp = new GTreeItem); Tmp->SetText("Loading..."); } } GString VcLeaf::Full() { GFile::Path p(Path); p += Leaf; return p.GetFull(); } void VcLeaf::OnBrowse() { Parent->FolderStatus(Full(), this); } void VcLeaf::AfterBrowse() { LList *Files = d->Files; Files->Empty(); GDirectory Dir; for (int b = Dir.First(Full()); b; b = Dir.Next()) { if (Dir.IsDir()) continue; VcFile *f = new VcFile(d, Parent, NULL); if (f) { // f->SetText(COL_STATE, f->SetText(Dir.GetName(), COL_FILENAME); Files->Insert(f); } } Files->ResizeColumnsToContent(); } void VcLeaf::OnExpand(bool b) { if (Tmp && b) { Tmp->Remove(); DeleteObj(Tmp); GFile::Path p(Path); p += Leaf; Parent->ReadDir(this, p); } } char *VcLeaf::GetText(int Col) { if (Col == 0) return Leaf; return NULL; } int VcLeaf::GetImage(int Flags) { return Folder ? IcoFolder : IcoFile; } int VcLeaf::Compare(VcLeaf *b) { // Sort folders to the top... if (Folder ^ b->Folder) return (int)b->Folder - (int)Folder; // Then alphabetical return Stricmp(Leaf.Get(), b->Leaf.Get()); } bool VcLeaf::Select() { return GTreeItem::Select(); } void VcLeaf::Select(bool b) { GTreeItem::Select(b); if (b) OnBrowse(); } void VcLeaf::OnMouseClick(GMouse &m) { if (m.IsContextMenu()) { GSubMenu s; s.AppendItem("Log", IDM_LOG); s.AppendItem("Blame", IDM_BLAME, !Folder); int Cmd = s.Float(GetTree(), m); switch (Cmd) { case IDM_LOG: { break; } case IDM_BLAME: { Parent->Blame(Full()); break; } } } } diff --git a/include/common/GArray.h b/include/common/GArray.h --- a/include/common/GArray.h +++ b/include/common/GArray.h @@ -1,809 +1,803 @@ /// \file /// \author Matthew Allen /// \brief Growable, type-safe array. #ifndef _GARRAY_H_ #define _GARRAY_H_ #include #include #define GARRAY_MIN_SIZE 16 #if defined(LGI_CHECK_MALLOC) && !defined(LGI_MEM_DEBUG) #error "Include GMem.h first" #endif #if defined(PLATFORM_MINGW) #ifdef __cplusplus extern "C" { #endif #ifndef _QSORT_S_DEFINED #define _QSORT_S_DEFINED _CRTIMP void __cdecl qsort_s(void *_Base, size_t _NumOfElements, size_t _SizeOfElements, int (__cdecl *_PtFuncCompare)(void *,const void *,const void *), void *_Context); #endif #ifdef __cplusplus } #endif #endif #include "GRange.h" /// \brief Growable type-safe array. /// \ingroup Base /// /// You can store simple objects inline in this array, but all their contents are initialized /// to the octet 0x00. Which limits use to objects that don't have a virtual table and /// don't need construction (constructors are not called). /// /// The objects are copied around during use so you can't assume their pointer /// will remain the same over time either. However when objects are deleted from the /// array their destructors WILL be called. This allows you to have simple objects that /// have dynamically allocated pointers that are freed properly. A good example of this /// type of object is the GVariant or GAutoString class. /// /// If you want to store objects with a virtual table, or that need their constructor /// to be called then you should create the GArray with pointers to the objects instead /// of inline objects. And to clean up the memory you can call GArray::DeleteObjects or /// GArray::DeleteArrays. template class GArray { Type *p; size_t len; size_t alloc; #ifdef _DEBUG public: int GetAlloc() { return alloc; } #endif protected: bool fixed; public: typedef Type ItemType; /// Constructor GArray(size_t PreAlloc = 0) { p = 0; alloc = len = PreAlloc; fixed = false; if (alloc) { size_t Bytes = sizeof(Type) * alloc; p = (Type*) malloc(Bytes); if (p) { memset(p, 0, Bytes); } else { alloc = len = 0; } } } GArray(const GArray &c) { p = 0; alloc = len = 0; fixed = false; *this = c; } /// Destructor ~GArray() { Length(0); } /// Does a range check on a pointer... /// \returns true if the pointer is pointing to a valid object /// in this array. bool PtrCheck(void *Ptr) { return p != NULL && Ptr >= p && Ptr < &p[len]; } /// Does a range check on an index... bool IdxCheck(ssize_t i) { return i >= 0 && i < (ssize_t)len; } /// Returns the number of used entries size_t Length() const { return len; } /// Makes the length fixed.. void SetFixedLength(bool fix = true) { fixed = fix; } /// Emtpies the array of all objects. bool Empty() { return Length(0); } /// Sets the length of available entries bool Length(size_t i) { if (i > 0) { if (i > len && fixed) { assert(!"Attempt to enlarged fixed array."); return false; } size_t nalloc = alloc; if (i < len) { // Shrinking } else { // Expanding nalloc = 0x10; while (nalloc < i) nalloc <<= 1; assert(nalloc >= i); } if (nalloc != alloc) { Type *np = (Type*)malloc(sizeof(Type) * nalloc); if (!np) { return false; } memset(np + len, 0, (nalloc - len) * sizeof(Type)); if (p) { // copy across common elements memcpy(np, p, MIN(len, i) * sizeof(Type)); free(p); } p = np; alloc = nalloc; } if (i > len) { // zero new elements memset(p + len, 0, sizeof(Type) * (nalloc - len)); } else if (i < len) { for (size_t n=i; n &operator =(const GArray &a) { Length(a.Length()); if (p && a.p) { for (uint32_t i=0; i= 0 && (uint32_t)i < len) return p[i]; static Type t; return t; } // Returns the address of an item or NULL if index is out of range Type *AddressOf(size_t i = 0) { return i < len ? p + i : NULL; } /// \brief Returns a reference a given entry. /// /// If the entry is off the end of the array and "fixed" is false, /// it will grow to make it valid. Type &operator [](size_t i) { static Type t; if ( (fixed && (uint32_t)i >= len) ) { memset(&t, 0, sizeof(t)); if (fixed && (uint32_t)i >= len) { assert(!"Attempt to enlarged fixed array."); } return t; } if (i >= (int)alloc) { // increase array length size_t nalloc = MAX(alloc, GARRAY_MIN_SIZE); while (nalloc <= (uint32_t)i) { nalloc <<= 1; } #if 0 if (nalloc > 1<<30) { #if defined(_DEBUG) && defined(_MSC_VER) LgiAssert(0); #endif ZeroObj(t); return t; } #endif // alloc new array Type *np = (Type*) malloc(sizeof(Type) * nalloc); if (np) { // clear new cells memset(np + len, 0, (nalloc - len) * sizeof(Type)); if (p) { // copy across old cells memcpy(np, p, len * sizeof(Type)); // clear old array free(p); } // new values p = np; alloc = nalloc; } else { static Type *t = 0; return *t; } } // adjust length of the the array if ((uint32_t)i + 1 > len) { len = i + 1; } return p[i]; } /// Delete all the entries as if they are pointers to objects void DeleteObjects() { if (len > 0) { size_t InitialLen = len; delete p[0]; if (InitialLen == len) { // Non self deleting for (uint i=1; i= 0; } /// Removes last element bool PopLast() { if (len <= 0) return false; return Length(len - 1); } /// Deletes an entry bool DeleteAt ( /// The index of the entry to delete size_t Index, /// true if the order of the array matters, otherwise false. bool Ordered = false ) { if (p && Index < len) { // Delete the object p[Index].~Type(); // Move the memory up if (Index < len - 1) { if (Ordered) { memmove(p + Index, p + Index + 1, (len - Index - 1) * sizeof(Type) ); } else { p[Index] = p[len-1]; } } // Adjust length len--; // Kill the element at the end... otherwise New() returns non-zero data. memset(p + len, 0, sizeof(Type)); return true; } return false; } /// Deletes the entry 'n' bool Delete ( /// The value of the entry to delete Type n, /// true if the order of the array matters, otherwise false. bool Ordered = false ) { ssize_t i = IndexOf(n); if (p && i >= 0) { return DeleteAt(i, Ordered); } return false; } /// Appends an element void Add ( /// Item to insert const Type &n ) { (*this)[len] = n; } /// Appends multiple elements bool Add ( /// Items to insert Type *s, /// Length of array ssize_t count ) { if (!s || count < 1) return false; ssize_t i = len; if (!Length(len + count)) return false; Type *d = p + i; while (count--) { *d++ = *s++; } return true; } /// Appends an array of elements bool Add ( /// Array to insert GArray &a ) { ssize_t old = len; if (Length(len + a.Length())) { for (unsigned i=0; i &operator +(GArray &a) { Add(a); return *this; } /// Inserts an element into the array bool AddAt ( /// Item to insert before size_t Index, /// Item to insert Type n ) { // Make room - if (Length(len + 1)) - { - if (Index >= 0 && (uint32_t)Index < len - 1) - { - // Shift elements after insert point up one - memmove(p + Index + 1, p + Index, (len - Index - 1) * sizeof(Type) ); - } - else - { - // Add at the end, not after the end... - Index = len - 1; - } + if (!Length(len + 1)) + return false; - // Insert item - memset(p + Index, 0, sizeof(*p)); - p[Index] = n; + if (Index < len - 1) + // Shift elements after insert point up one + memmove(p + Index + 1, p + Index, (len - Index - 1) * sizeof(Type) ); + else + // Add at the end, not after the end... + Index = len - 1; - return true; - } + // Insert item + memset(p + Index, 0, sizeof(*p)); + p[Index] = n; - return false; + return true; } /// Sorts the array void Sort(int (*Compare)(Type*, Type*)) { typedef int (*qsort_compare)(const void *, const void *); qsort(p, len, sizeof(Type), (qsort_compare)Compare); } // Sorts the array with a comparison function (can I get a standard here?) #if defined(_MSC_VER) || defined(PLATFORM_MINGW) #define DeclGArrayCompare(func_name, type, user_type) \ int func_name(user_type *param, type *a, type *b) template void Sort(int (*Compare)(U *user_param, Type*, Type*), U *user_param) { typedef int (*qsort_s_compare)(void *, const void *, const void *); qsort_s(p, len, sizeof(Type), (qsort_s_compare)Compare, user_param); } #elif defined(MAC) #define DeclGArrayCompare(func_name, type, user_type) \ int func_name(user_type *param, type *a, type *b) template void Sort(int (*Compare)(U *user_param, Type*, Type*), U *user_param) { typedef int (*qsort_r_compare)(void *, const void *, const void *); qsort_r(p, len, sizeof(Type), user_param, (qsort_r_compare)Compare); } #elif !defined(BEOS) // POSIX? #define DeclGArrayCompare(func_name, type, user_type) \ int func_name(type *a, type *b, user_type *param) template void Sort(int (*Compare)(Type*, Type*, T *user_param), T *user_param) { typedef int (*qsort_r_compare)(const void *, const void *, void *); qsort_r(p, len, sizeof(Type), (qsort_r_compare)Compare, user_param); } #endif /// \returns a reference to a new object on the end of the array /// /// Never assign this to an existing variable. e.g: /// GArray a; /// MyObject &o = a.New(); /// o.Type = something; /// o = a.New(); /// o.Type = something else; /// /// This causes the first object to be overwritten with a blank copy. Type &New() { return (*this)[len]; } /// Returns the memory held by the array and sets itself to empty Type *Release() { Type *Ptr = p; p = 0; len = alloc = 0; return Ptr; } void Swap(GArray &other) { LSwap(p, other.p); LSwap(len, other.len); LSwap(alloc, other.alloc); } /// Swaps a range of elements between this array and 'b' bool SwapRange ( // The range of 'this' to swap out GRange aRange, // The other array to swap with GArray &b, // The range of 'b' to swap with this array GRange bRange ) { GArray Tmp; // Store entries in this that will be swapped Tmp.Add(AddressOf(aRange.Start), aRange.Len); // Copy b's range into this ssize_t Common = MIN(bRange.Len, aRange.Len); ssize_t aIdx = aRange.Start; ssize_t bIdx = bRange.Start; for (int i=0; i bRange.Len) { // Shrink the range in this to fit 'b' ssize_t Del = aRange.Len - bRange.Len; for (ssize_t i=0; i bRange.Len) { // Grow range to fit this ssize_t Add = aRange.Len - bRange.Len; for (ssize_t i=0; i class Iter { ssize_t i; char each_dir; GArray *a; public: Iter(GArray *arr) // 'End' constructor { i = -1; a = arr; each_dir = 0; } Iter(GArray *arr, size_t pos) { i = pos; a = arr; each_dir = 0; } bool operator ==(const Iter &it) const { int x = (int)In() + (int)it.In(); if (x == 2) return (a == it.a) && (i == it.i); return x == 0; } bool operator !=(const Iter &it) const { return !(*this == it); } operator bool() const { return In(); } bool In() const { return i >= 0 && i < (ssize_t)a->Length(); } bool End() const { return i < 0 || i >= a->Length(); } T &operator *() { return (*a)[i]; } Iter &operator ++() { i++; return *this; } Iter &operator --() { i--; return *this; } Iter &operator ++(int) { i++; return *this; } Iter &operator --(int) { i--; return *this; } }; typedef Iter I; I begin() { return I(this, 0); } I rbegin() { return I(this, len-1); } I end() { return I(this); } /* To use this iteration method in a for loop: for (T *Ptr = NULL; Array.Iterate(Ptr); ) { // Use 'Ptr' here } */ bool Iterate(Type *&Ptr) { if (!len || !p) return false; return Ptr ? PtrCheck(++Ptr) : (Ptr = p) != NULL; } /* To use this iteration method in a for loop: for (T *Ptr = NULL, size_t Idx; Array.Iterate(Idx, Ptr); ) { // Use 'Ptr' here } */ template bool IteratePtr(size_t &Idx, T &Ptr) { if (!len || !p) return false; if (!Ptr) { Ptr = dynamic_cast(*p); Idx = 0; return true; } Ptr = p + (++Idx); return PtrCheck(Ptr); } }; #endif diff --git a/include/common/GBox.h b/include/common/GBox.h --- a/include/common/GBox.h +++ b/include/common/GBox.h @@ -1,51 +1,52 @@ #ifndef _GBOX_H_ #define _GBOX_H_ #include "GCss.h" /// This is a vertical or horizontal layout box, similar to the /// old GSplitter control except it can handle any number of children class LgiClass GBox : public GView { struct GBoxPriv *d; protected: public: struct Spacer { GRect Pos; // Position on screen in view coords GColour Colour; // Colour of the spacer uint32_t SizePx; // Size in pixels }; GBox(int Id = -1, bool Vertical = false, const char *name = NULL); ~GBox(); const char *GetClass() { return "GBox"; } bool IsVertical(); void SetVertical(bool v); Spacer *GetSpacer(int i); GViewI *GetViewAt(int i); bool SetViewAt(uint32_t i, GViewI *v); int64 Value(); void Value(int64 i); void OnCreate(); void OnPaint(GSurface *pDC); void OnPosChange(); void OnMouseClick(GMouse &m); bool OnViewMouse(GView *v, GMouse &m); void OnMouseMove(GMouse &m); void OnChildrenChanged(GViewI *Wnd, bool Attaching); GMessage::Result OnEvent(GMessage *Msg); bool Pour(GRegion &r); LgiCursor GetCursor(int x, int y); bool OnLayout(GViewLayoutInfo &Inf); + int OnNotify(GViewI *Ctrl, int Flags); bool Serialize(GDom *Dom, const char *OptName, bool Write); bool SetSize(int ViewIndex, GCss::Len Size); }; -#endif \ No newline at end of file +#endif diff --git a/include/common/LDateTime.h b/include/common/LDateTime.h --- a/include/common/LDateTime.h +++ b/include/common/LDateTime.h @@ -1,395 +1,396 @@ /// \file /// \author Matthew Allen /** * \defgroup Time Time and date handling * \ingroup Lgi */ #ifndef __DATE_TIME_H #define __DATE_TIME_H #include #include "GStringClass.h" #define GDTF_DEFAULT 0 /// Format the date as DD/MM/YYYY /// \ingroup Time #define GDTF_DAY_MONTH_YEAR 0x001 /// Format the date as MM/DD/YYYY /// \ingroup Time #define GDTF_MONTH_DAY_YEAR 0x002 /// Format the date as YYYY/MM/DD /// \ingroup Time #define GDTF_YEAR_MONTH_DAY 0x004 /// The bit mask for the date /// \ingroup Time #define GDTF_DATE_MASK 0x00f /// Format the time as HH:MM and an am/pm marker /// \ingroup Time #define GDTF_12HOUR 0x010 /// Format the time as 24 hour time /// \ingroup Time #define GDTF_24HOUR 0x020 /// The bit mask for the time /// \ingroup Time #define GDTF_TIME_MASK 0x0f0 /// Format the date with a leading zero /// \ingroup Time #define GDTF_DAY_LEADINGZ 0x100 /// Format the month with a leading zero /// \ingroup Time #define GDTF_MONTH_LEADINGZ 0x200 /// A date/time class /// /// This class interacts with system times represented as 64bit ints. The various OS support different /// formats for that 64bit int values. On windows the system times are in 100-nanosecond intervals since /// January 1, 1601 (UTC), as per the FILETIME structure, on Posix systems (Linux/Mac) the 64bit values /// are in milliseconds since January 1, 1970 UTC. This is just unix time * 1000. If you are serializing /// these 64bit values you should take that into account, they are NOT cross platform. The GDirectory class /// uses the same 64bit values as accepted here for the file's last modified timestamp etc. To convert the /// 64bit values to seconds, divide by LDateTime::Second64Bit, useful for calculating the time in seconds /// between 2 LDateTime objects. /// /// \ingroup Time class LgiClass LDateTime // This class can't have a virtual table, because it's used in // GArray's which initialize with all zero bytes. { /// 1 - DaysInMonth int16 _Day; /// #### int16 _Year; /// Milliseconds: 0-999 int16 _Thousands; /// 1-12 int16 _Month; /// 0-59 int16 _Seconds; /// 0-59 int16 _Minutes; /// 0-23 (24hr) int16 _Hours; /// The current timezone of this object, defaults to the system timezone int16 _Tz; // in minutes (+10 == 600 etc) /// Combination of (#GDTF_DAY_MONTH_YEAR or #GDTF_MONTH_DAY_YEAR or #GDTF_YEAR_MONTH_DAY) and (#GDTF_12HOUR or #GDTF_24HOUR) uint16 _Format; /// The default formatting of datetimes static uint16 DefaultFormat; /// The default date separator character static char DefaultSeparator; public: LDateTime(const char *Init = NULL); LDateTime(const LDateTime &dt) { *this = dt; } ~LDateTime(); enum { /// Resolution of a second when using 64 bit int's /// \sa LDateTime::Get(int64), LDateTime::Set(int64) #ifdef WIN32 Second64Bit = 10000000, #else Second64Bit = 1000, #endif }; /// Returns true if all the components are in a valid range bool IsValid(); /// Sets the date to an NULL state void Empty(); /// Returns the day int Day() { return _Day; } /// Sets the day void Day(int d) { _Day = d; } /// Returns the month int Month() { return _Month; } /// Sets the month void Month(int m) { _Month = m; } /// Sets the month by it's name void Month(char *m); /// Returns the year int Year() { return _Year; } /// Sets the year void Year(int y) { _Year = y; } /// Returns the millisecond part of the time int Thousands() { return _Thousands; } /// Sets the millisecond part of the time void Thousands(int t) { _Thousands = t; } /// Returns the seconds part of the time int Seconds() { return _Seconds; } /// Sets the seconds part of the time void Seconds(int s) { _Seconds = s; } /// Returns the minutes part of the time int Minutes() { return _Minutes; } /// Sets the minutes part of the time void Minutes(int m) { _Minutes = m; } /// Returns the hours part of the time int Hours() { return _Hours; } /// Sets the hours part of the time void Hours(int h) { _Hours = h; } /// Returns the timezone of this current date time object in minutes (+10 = 600) int GetTimeZone() { return _Tz; } /// Returns the timezone in hours double GetTimeZoneHours() { return (double)_Tz / 60.0; } /// Sets the timezone of this current object.in minutes (+10 = 600) void SetTimeZone ( /// The new timezone int Tz, /// True if you want to convert the date and time to the new zone, /// False if you want to leave the date/time as it is. bool ConvertTime ); /// Set this object to UTC timezone, changing the other members as /// needed LDateTime &ToUtc(bool AssumeLocal = false) { if (AssumeLocal) _Tz = SystemTimeZone(); SetTimeZone(0, true); return *this; } /// \returns the UTC version of this object. LDateTime Utc() const { LDateTime dt = *this; return dt.ToUtc(); } /// Changes the timezone to the local zone, changing other members /// as needed. LDateTime &ToLocal(bool AssumeUtc = false) { if (AssumeUtc) _Tz = 0; SetTimeZone(SystemTimeZone(), true); return *this; } /// \returns the local time version of this object. LDateTime Local() { LDateTime dt = *this; return dt.ToLocal(); } /// Gets the current formatting of the date, the format only effects /// the representation of the date when converted to/from a string. /// \returns a bit mask of (#GDTF_DAY_MONTH_YEAR or #GDTF_MONTH_DAY_YEAR or #GDTF_YEAR_MONTH_DAY) and (#GDTF_12HOUR or #GDTF_24HOUR) uint16 GetFormat() { return _Format; } /// Sets the current formatting of the date, the format only effects /// the representation of the date when converted to/from a string void SetFormat ( /// a bit mask of (#GDTF_DAY_MONTH_YEAR or #GDTF_MONTH_DAY_YEAR or #GDTF_YEAR_MONTH_DAY) and (#GDTF_12HOUR or #GDTF_24HOUR) uint16 f ) { _Format = f; } /// \returns zero based index of weekday, or -1 if not found. static int IsWeekDay(const char *s); /// \returns zero based index of month, or -1 if not found. static int IsMonth(const char *s); /// The default format for the date when formatted as a string static uint16 GetDefaultFormat(); /// Sets the default format for the date when formatted as a string static void SetDefaultFormat(uint16 f) { DefaultFormat = f; } /// Gets the data and time as a GString GString Get(); /// Gets the date and time as a string /// \sa LDateTime::GetFormat() void Get(char *Str, size_t SLen); /// Gets the data and time as a 64 bit int (os specific) bool Get(uint64 &s); /// Gets just the date as a string /// \sa LDateTime::GetFormat() /// \returns The number of characters written to 'Str' int GetDate(char *Str, size_t SLen); /// Gets just the date as a GString /// \sa LDateTime::GetFormat() GString GetDate(); /// Gets just the time as a string /// \sa LDateTime::GetFormat() /// \returns The number of characters written to 'Str' int GetTime(char *Str, size_t SLen); /// Gets just the time as a GString /// \sa LDateTime::GetFormat() GString GetTime(); /// Returns the 64bit timestamp. uint64 Ts(); /// Sets the date and time to the system clock void SetNow(); /// Parses a date time from a string /// \sa LDateTime::GetFormat() bool Set(const char *Str); /// Sets the date and time from a 64 bit int (os specific) bool Set(uint64 s); + bool SetUnix(uint64 s); // Assume unix timestamp /// Sets the time from a time_t bool Set(time_t tt); /// Parses the date from a string /// \sa LDateTime::GetFormat() bool SetDate(const char *Str); /// Parses the time from a string /// \sa LDateTime::GetFormat() bool SetTime(const char *Str); /// Parses the date time from a free form string bool Parse(GString s); /// \returns true if 'd' is on the same day as this object bool IsSameDay(LDateTime &d); /// \returns true if 'd' is on the same month as this object bool IsSameMonth(LDateTime &d); /// \returns true if 'd' is on the same year as this object bool IsSameYear(LDateTime &d); /// \returns whether a year is a leap year or not bool IsLeapYear ( /// Pass a specific year here, or ignore to return if the current Date/Time is in a leap year. int Year = -1 ); /// Returns the day of the week as an index, 0=sun, 1=mon, 2=teus etc int DayOfWeek(); /// \returns the number of days in the current month int DaysInMonth(); /// Adds a number of seconds to the current date/time void AddSeconds(int64 Seconds); /// Adds a number of minutes to the current date/time void AddMinutes(int64 Minutes); /// Adds a number of hours to the current date/time void AddHours(int64 Hours); /// Adds a number of days to the current date/time bool AddDays(int64 Days); /// Adds a number of months to the current date/time void AddMonths(int64 Months); /// The system timezone including daylight savings offset in minutes, +10 would return 600 static int SystemTimeZone(bool ForceUpdate = false); /// Any daylight savings offset applied to TimeZone(), in minutes. e.g. to retreive the /// timezone uneffected by DST use TimeZone() - TimeZoneOffset(). static int SystemTimeZoneOffset(); /// Daylight savings info record struct LgiClass GDstInfo { /// Timestamp where the DST timezone changes to 'Offset' uint64 UtcTimeStamp; /// The new offset in minutes (e.g. 600 = +10 hours) int Offset; LDateTime GetLocal(); }; /// Retreives daylight savings start and end events for a given period. One event will be emitted /// for the current DST/TZ setting at the datetime specified by 'Start', followed by any changes that occur /// between that and the 'End' datetime. To retreive just the DST info for start, use NULL for end. static bool GetDaylightSavingsInfo ( /// [Out] The array to receive DST info. At minimum one record will be returned /// matching the TZ in place for the start datetime. GArray &Out, /// [In] The start date that you want DST info for. LDateTime &Start, /// [Optional In] The end of the period you want DST info for. LDateTime *End = 0 ); /// Decodes an email date into the current instance bool Decode(const char *In); /// Returns a month index from a month name static int MonthFromName(const char *Name); // File int Sizeof(); bool Serialize(class GFile &f, bool Write); bool Serialize(class GDom *Props, char *Name, bool Write); // operators bool operator <(LDateTime &dt) const; bool operator <=(LDateTime &dt) const; bool operator >(LDateTime &dt) const; bool operator >=(LDateTime &dt) const; bool operator ==(const LDateTime &dt) const; bool operator !=(LDateTime &dt) const; int Compare(const LDateTime *d) const; LDateTime operator -(LDateTime &dt); LDateTime operator +(LDateTime &dt); int DiffMonths(LDateTime &dt); operator uint64() { uint64 ts = 0; Get(ts); return ts; } LDateTime &operator =(uint64 ts) { Set(ts); return *this; } LDateTime &operator =(struct tm *t); LDateTime &operator =(const LDateTime &t); LDateTime &operator =(LDateTime const *t) { if (t) *this = *t; return *this; } /// GDom interface. /// /// Even though we don't inherit from a GDom class this class supports the same /// interface for ease of use. Currently there are cases where LDateTime is used /// in GArray's which don't implement calling a constructor (they init with all /// zeros). bool GetVariant(const char *Name, class GVariant &Value, char *Array = NULL); bool SetVariant(const char *Name, class GVariant &Value, char *Array = NULL); bool CallMethod(const char *Name, class GVariant *ReturnValue, GArray &Args); }; /// Time zone information struct GTimeZone { public: /// The offset from UTC float Offset; /// The name of the zone const char *Text; }; /// A list of all known timezones. extern GTimeZone GTimeZones[]; #ifdef _DEBUG LgiFunc bool LDateTime_Test(); #endif #endif diff --git a/include/common/LOAuth2.h b/include/common/LOAuth2.h new file mode 100644 --- /dev/null +++ b/include/common/LOAuth2.h @@ -0,0 +1,64 @@ +#ifndef _LOAUTH2_H_ +#define _LOAUTH2_H_ + +////////////////////////////////////////////////////////////////// + +/* Do this somewhere? +GAutoString ErrorMsg; +StartSSL(ErrorMsg, NULL); +*/ + +class LOAuth2 +{ + struct LOAuth2Priv *d; + +public: + struct Params + { + enum ServiceProvider + { + None, + OAuthGoogle, + OAuthMicrosoft, + } Provider; + + GString ClientID; + GString ClientSecret; + GString RedirURIs; + GString AuthUri; + GString ApiUri; + // GString RevokeUri; + GString Scope; + GUri Proxy; + + // GString AccessToken; + // GString RefreshToken; + // int ExpiresIn; + + Params() + { + Provider = None; + // ExpiresIn = 0; + } + + bool IsValid() + { + return Provider != None && + ClientID && + ClientSecret && + RedirURIs && + AuthUri && + // RevokeUri && + Scope && + ApiUri; + } + }; + + + LOAuth2(Params ¶ms, const char *account, GDom *store, GStream *log = NULL); + virtual ~LOAuth2(); + + GString GetAccessToken(); +}; + +#endif \ No newline at end of file diff --git a/include/common/Mail.h b/include/common/Mail.h --- a/include/common/Mail.h +++ b/include/common/Mail.h @@ -1,979 +1,951 @@ /** \file \author Matthew Allen */ #ifndef __MAIL_H #define __MAIL_H #include "INet.h" #include "Base64.h" #include "Progress.h" #include "GVariant.h" +#include "LOAuth2.h" #ifndef GPL_COMPATIBLE #define GPL_COMPATIBLE 0 #endif // Defines #define MAX_LINE_SIZE 1024 #define MAX_NAME_SIZE 64 #define EMAIL_LINE_SIZE 76 // #define IsDigit(c) ((c) >= '0' AND (c) <= '9') // MIME content types #define CONTENT_NONE 0 #define CONTENT_BASE64 1 #define CONTENT_QUOTED_PRINTABLE 2 #define CONTENT_OCTET_STREAM 3 // Mail logging defines #define MAIL_SEND_COLOUR Rgb24(0, 0, 0xff) #define MAIL_RECEIVE_COLOUR Rgb24(0, 0x8f, 0) #define MAIL_ERROR_COLOUR Rgb24(0xff, 0, 0) #define MAIL_WARNING_COLOUR Rgb24(0xff, 0x7f, 0) #define MAIL_INFO_COLOUR Rgb24(0, 0, 0) // Helper functions extern void TokeniseStrList(char *Str, List &Output, const char *Delim); extern char ConvHexToBin(char c); #define ConvBinToHex(i) (((i)<10)?'0'+(i):'A'+(i)-10) extern void DecodeAddrName(const char *Start, GAutoString &Name, GAutoString &Addr, const char *DefaultDomain); extern char *DecodeRfc2047(char *Str); extern char *EncodeRfc2047(char *Str, const char *CodePage, List *CharsetPrefs, ssize_t LineLength = 0); extern char *DecodeBase64Str(char *Str, int Len = -1); extern char *DecodeQuotedPrintableStr(char *Str, ssize_t Len = -1); extern bool Is8Bit(char *Text); extern int MaxLineLen(char *Text); extern char *EncodeImapString(const char *s); extern char *DecodeImapString(const char *s); +extern bool UnBase64Str(GString &s); +extern bool Base64Str(GString &s); extern const char *sTextPlain; extern const char *sTextHtml; extern const char *sTextXml; extern const char *sApplicationInternetExplorer; extern const char sMultipartMixed[]; extern const char sMultipartEncrypted[]; extern const char sMultipartSigned[]; extern const char sMultipartAlternative[]; extern const char sMultipartRelated[]; extern const char sAppOctetStream[]; // Classes class MailProtocol; struct MailProtocolError { int Code; GString ErrMsg; MailProtocolError() { Code = 0; } }; class MailProtocolProgress { public: uint64 Start; int Value; ssize_t Range; MailProtocolProgress() { Empty(); } void Empty() { Start = 0; Value = 0; Range = 0; } void StartTransfer(ssize_t Size) { Start = LgiCurrentTime(); Value = 0; Range = Size; } }; class LogEntry { GColour c; public: GArray Txt; LogEntry(GColour col); GColour GetColour() { return c; } bool Add(const char *t, ssize_t len = -1); }; /// Attachment descriptor class FileDescriptor : public GBase { protected: // Global int64 Size; char *MimeType; char *ContentId; // Read from file GFile File; GStreamI *Embeded; bool OwnEmbeded; int64 Offset; LMutex *Lock; // Write to memory uchar *Data; GAutoPtr DataStream; public: FileDescriptor(GStreamI *embed, int64 Offset, int64 Size, char *Name); FileDescriptor(char *name); FileDescriptor(char *data, int64 len); FileDescriptor(); ~FileDescriptor(); void SetLock(LMutex *l); LMutex *GetLock(); void SetOwnEmbeded(bool i); // Access functions GStreamI *GotoObject(); // Get data to read uchar *GetData(); // Get data from write int Sizeof(); char *GetMimeType() { return MimeType; } void SetMimeType(char *s) { DeleteArray(MimeType); MimeType = NewStr(s); } char *GetContentId() { return ContentId; } void SetContentId(char *s) { DeleteArray(ContentId); ContentId = NewStr(s); } // Decode MIME data to memory bool Decode(char *ContentType, char *ContentTransferEncoding, char *MimeData, int MimeDataLength); }; /// Address dscriptor class AddressDescriptor : public GBase { public: uint8_t Status; uchar CC; // MAIL_ADDR_?? char *Name; char *Addr; void *Data; // application defined AddressDescriptor(AddressDescriptor *Copy = 0); ~AddressDescriptor(); void _Delete(); void Print(char *Str, int Len); ssize_t Sizeof() { return SizeofStr(Name) + SizeofStr(Addr); } bool Serialize(GFile &f, bool Write) { bool Status = true; if (Write) { WriteStr(f, Name); WriteStr(f, Addr); } else { DeleteArray(Name); Name = ReadStr(f PassDebugArgs); DeleteArray(Addr); Addr = ReadStr(f PassDebugArgs); } return Status; } }; /* class MailMessage : public GStream { char* Text; char* TextCharset; char* Html; char* HtmlCharset; public: List To; AddressDescriptor *From; AddressDescriptor *Reply; GAutoString Subject; GAutoString MessageID; GAutoString FwdMsgId; GAutoString BounceMsgId; List FileDesc; char* InternetHeader; char Priority; int MarkColour; uint8 DispositionNotificationTo : 1; // read receipt uint8 EncryptedMsg : 1; GAutoString References; // Protocol specific GAutoString UserData; // Class MailMessage(); virtual ~MailMessage(); void Empty(); virtual char *GetBody(); virtual bool SetBody(const char *Txt, int Bytes = -1, bool Copy = true, const char *Cs = 0); virtual char *GetBodyCharset(); virtual bool SetBodyCharset(const char *Cs); virtual char *GetHtml(); virtual bool SetHtml(const char *Txt, int Bytes = -1, bool Copy = true, const char *Cs = 0); virtual char *GetHtmlCharset(); virtual bool SetHtmlCharset(const char *Cs); // Logging GStream *Log; int Write(const void *Ptr, int Size, int Flags = 0); // Conversion to/from MIME GStringPipe *Raw; // High level encoding functions bool Encode (GStreamI &Out, GStream *HeadersSink, MailProtocol *Protocol, bool Mime = true); bool EncodeHeaders (GStreamI &Out, MailProtocol *Protocol, bool Mime = true); bool EncodeBody (GStreamI &Out, MailProtocol *Protocol, bool Mime = true); // Encoding mime segment data int EncodeText (GStreamI &Out, GStreamI &In); int EncodeQuotedPrintable (GStreamI &Out, GStreamI &In); int EncodeBase64 (GStreamI &Out, GStreamI &In); }; */ /// Base class for mail protocol implementations class MailProtocol { protected: char Buffer[4<<10]; LMutex SocketLock; GAutoPtr Socket; + LOAuth2::Params OAuth2; + GDom *SettingStore; bool Error(const char *file, int line, const char *msg, ...); bool Read(); bool Write(const char *Buf = NULL, bool Log = false); virtual void OnUserMessage(char *Str) {} public: // Logging GStreamI *Logger; void Log(const char *Str, GSocketI::SocketMsgType type); // Task Progress MailProtocolProgress *Items; MailProtocolProgress *Transfer; // Settings int ErrMsgId; /// \sa #L_ERROR_ESMTP_NO_AUTHS, #L_ERROR_ESMTP_UNSUPPORTED_AUTHS GString ErrMsgFmt; /// The format for the printf GString ErrMsgParam; /// The arguments for the printf GString ProgramName; GString ExtraOutgoingHeaders; List CharsetPrefs; // Object MailProtocol(); virtual ~MailProtocol(); // Methods - // GSocketI *GetSocket() { return Socket; } + void SetOAuthParams(LOAuth2::Params &p) { OAuth2 = p; } + void SetSettingStore(GDom *store) { SettingStore = store; } /// Thread safe hard close (quit now) bool CloseSocket() { LMutex::Auto l(&SocketLock, _FL); if (Socket != NULL) return Socket->Close() != 0; return false; } void SetError(int ResourceId, const char *Fmt, const char *Param = NULL) { ErrMsgId = ResourceId; ErrMsgFmt = Fmt; ErrMsgParam = Param; } }; ///////////////////////////////////////////////////////////////////// // Mail IO parent classes /// Enable STARTTLS support (requires an SSL capable socket) #define MAIL_USE_STARTTLS 0x01 /// Use authentication #define MAIL_USE_AUTH 0x02 /// Force the use of PLAIN type authentication #define MAIL_USE_PLAIN 0x04 /// Force the use of LOGIN type authentication #define MAIL_USE_LOGIN 0x08 /// Force the use of NTLM type authentication #define MAIL_USE_NTLM 0x10 /// Secure auth #define MAIL_SECURE_AUTH 0x20 /// Use SSL #define MAIL_SSL 0x40 /// OAUTH2 #define MAIL_USE_OAUTH2 0x80 /// CRAM-MD5 #define MAIL_USE_CRAM_MD5 0x100 /// Mail sending protocol class MailSink : public MailProtocol { public: /// Connection setup/shutdown virtual bool Open ( /// The transport layer to use GSocketI *S, /// The host to connect to const char *RemoteHost, /// The local domain const char *LocalDomain, /// The sink username (or NULL) const char *UserName, /// The sink password (or NULL) const char *Password, /// The port to connect with or 0 for default. int Port, /// Options: Use any of #MAIL_SSL, #MAIL_USE_STARTTLS, #MAIL_SECURE_AUTH, #MAIL_USE_PLAIN, #MAIL_USE_LOGIN etc or'd together. int Flags ) = 0; /// Close the connection virtual bool Close() = 0; // Commands available while connected /// Write the email's contents into the GStringPipe returned from /// SendStart and then call SendEnd to finish the transaction virtual GStringPipe *SendStart(List &To, AddressDescriptor *From, MailProtocolError *Err = 0) = 0; /// Finishes the mail send virtual bool SendEnd(GStringPipe *Sink) = 0; }; struct ImapMailFlags { union { struct { uint8_t ImapAnswered : 1; uint8_t ImapDeleted : 1; uint8_t ImapDraft : 1; uint8_t ImapFlagged : 1; uint8_t ImapRecent : 1; uint8_t ImapSeen : 1; uint8_t ImapExpunged :1; }; uint16 All; }; ImapMailFlags(char *init = 0) { ImapAnswered = 0; ImapDeleted = 0; ImapDraft = 0; ImapFlagged = 0; ImapRecent = 0; ImapSeen = 0; ImapExpunged = 0; if (init) Set(init); } char *Get() { char s[256] = ""; int ch = 0; if (ImapAnswered) ch += sprintf_s(s+ch, sizeof(s)-ch, "\\answered "); if (ImapDeleted) ch += sprintf_s(s+ch, sizeof(s)-ch, "\\deleted "); if (ImapDraft) ch += sprintf_s(s+ch, sizeof(s)-ch, "\\draft "); if (ImapFlagged) ch += sprintf_s(s+ch, sizeof(s)-ch, "\\flagged "); if (ImapRecent) ch += sprintf_s(s+ch, sizeof(s)-ch, "\\recent "); if (ImapSeen) ch += sprintf_s(s+ch, sizeof(s)-ch, "\\seen "); if (ch == 0) return NULL; LgiAssert(ch < sizeof(s)); s[--ch] = 0; return NewStr(s); } void Set(const char *s) { All = 0; if (!s) s = ""; while (*s) { if (*s == '/' || *s == '\\') { while (*s == '/' || *s == '\\') s++; const char *e = s; while (*e && isalpha(*e)) e++; if (!_strnicmp(s, "answered", e-s)) ImapAnswered = true; else if (!_strnicmp(s, "deleted", e-s)) ImapDeleted = true; else if (!_strnicmp(s, "draft", e-s)) ImapDraft = true; else if (!_strnicmp(s, "flagged", e-s)) ImapFlagged = true; else if (!_strnicmp(s, "recent", e-s)) ImapRecent = true; else if (!_strnicmp(s, "seen", e-s)) ImapSeen = true; s = e; } else s++; } } bool operator ==(ImapMailFlags &f) { return ImapAnswered == f.ImapAnswered && ImapDeleted == f.ImapDeleted && ImapDraft == f.ImapDraft && ImapFlagged == f.ImapFlagged && ImapRecent == f.ImapRecent && ImapSeen == f.ImapSeen && ImapExpunged == f.ImapExpunged; } bool operator !=(ImapMailFlags &f) { return !(ImapAnswered == f.ImapAnswered && ImapDeleted == f.ImapDeleted && ImapDraft == f.ImapDraft && ImapFlagged == f.ImapFlagged && ImapRecent == f.ImapRecent && ImapSeen == f.ImapSeen && ImapExpunged == f.ImapExpunged); } }; /// A bulk mail handling class class MailTransaction { public: /// The index of the mail in the folder int Index; /// \sa #MAIL_POSTED_TO_GUI, #MAIL_EXPLICIT int Flags; // bool Delete; bool Status; bool Oversize; /// The mail protocol handler writes the email to this stream GStreamI *Stream; /// Flags used on the IMAP protocolf ImapMailFlags Imap; /// The user app can use this for whatever void *UserData; MailTransaction(); ~MailTransaction(); }; /// Return code from MailSrcCallback enum MailSrcStatus { /// Download the whole email DownloadAll, /// Download just the top part DownloadTop, /// Skip this email DownloadNone, /// About the whole receive DownloadAbort }; /// The callback function used by MailSource::Receive typedef MailSrcStatus (*MailSrcCallback) ( /// The currently executing transaction MailTransaction *Trans, /// The size of the email about to be downloaded uint64 Size, /// If DownloadTop is returned, you can set the number of lines to retreive here int *LinesToDownload, /// The data cookie passed into MailSource::Receive void *Data ); /// The callback function used by MailSource::Receive typedef bool (*MailReceivedCallback) ( /// The currently executing transaction MailTransaction *Trans, /// The data cookie passed into MailSource::Receive void *Data ); /// Collection of callbacks called during mail receive. You should zero this /// entire object before using it. Because if someone adds new callbacks after /// you write the calling code you wouldn't want to leave some callbacks un- /// initialized. A NULL callback is ignored. struct MailCallbacks { /// The callback data void *CallbackData; /// Called before receiving mail MailSrcCallback OnSrc; /// Called after mail received MailReceivedCallback OnReceive; }; /* /// Enable STARTTLS support (requires an SSL capable socket) #define MAIL_SOURCE_STARTTLS 0x01 /// Use authentication #define MAIL_SOURCE_AUTH 0x02 /// Force the use of PLAIN type authentication #define MAIL_SOURCE_USE_PLAIN 0x04 /// Force the use of LOGIN type authentication #define MAIL_SOURCE_USE_LOGIN 0x08 */ /// A generic mail source object class MailSource : public MailProtocol { public: /// Opens a connection to the server virtual bool Open ( /// The transport socket GSocketI *S, /// The hostname or IP of the server const char *RemoteHost, /// The port on the host to connect to int Port, /// The username for authentication const char *User, /// The password for authentication const char *Password, /// [Optional] Persistant storage of settings GDom *SettingStore, /// [Optional] Flags: #MAIL_SOURCE_STARTTLS, #MAIL_SOURCE_AUTH, #MAIL_SOURCE_USE_PLAIN, #MAIL_SOURCE_USE_LOGIN int Flags = 0) = 0; /// Closes the connection virtual bool Close() = 0; /// Returns the number of messages available on the server virtual int GetMessages() = 0; /// Receives a list of messages from the server. virtual bool Receive ( /// An array of messages to receive. The MailTransaction objects contains the index of the message to receive /// and various status values returned after the operation. GArray &Trans, /// An optional set of callback functions. MailCallbacks *Callbacks = 0 ) = 0; /// Deletes a message on the server virtual bool Delete(int Message) = 0; /// Gets the size of the message on the server virtual int Sizeof(int Message) = 0; /// Gets the size of all the messages on the server virtual bool GetSizes(GArray &Sizes) { return false; } /// Gets the unique identifier of the message virtual bool GetUid(int Message, char *Id, int IdLen) = 0; /// Gets the unique identifiers of a list of messages virtual bool GetUidList(List &Id) = 0; /// Gets the headers associated with a given message virtual char *GetHeaders(int Message) = 0; /// Sets the proxy server. e.g. HTTP mail. virtual void SetProxy(char *Server, int Port) {} }; ///////////////////////////////////////////////////////////////////// // Mail IO implementations /// SMTP implementation class MailSmtp : public MailSink { protected: bool ReadReply(const char *Str, GStringPipe *Pipe = 0, MailProtocolError *Err = 0); bool WriteText(const char *Str); public: MailSmtp(); ~MailSmtp(); bool Open(GSocketI *S, const char *RemoteHost, const char *LocalDomain, const char *UserName, const char *Password, int Port = 0, int Flags = 0); bool Close(); bool SendToFrom(List &To, AddressDescriptor *From, MailProtocolError *Err = 0); GStringPipe *SendData(MailProtocolError *Err = 0); GStringPipe *SendStart(List &To, AddressDescriptor *From, MailProtocolError *Err = 0); bool SendEnd(GStringPipe *Sink); // bool Send(MailMessage *Msg, bool Mime = false); }; class MailSendFolder : public MailSink { class MailPostFolderPrivate *d; public: MailSendFolder(char *Path); ~MailSendFolder(); bool Open(GSocketI *S, const char *RemoteHost, const char *LocalDomain, const char *UserName, const char *Password, int Port = 0, int Flags = 0); bool Close(); GStringPipe *SendStart(List &To, AddressDescriptor *From, MailProtocolError *Err = 0); bool SendEnd(GStringPipe *Sink); }; class MailPop3 : public MailSource { protected: bool ReadReply(); bool ReadMultiLineReply(char *&Str); int GetInt(); bool MailIsEnd(char *Ptr, ssize_t Len); bool ListCmd(const char *Cmd, LHashTbl, bool> &Results); const char *End; const char *Marker; int Messages; public: MailPop3(); ~MailPop3(); // Connection bool Open(GSocketI *S, const char *RemoteHost, int Port, const char *User, const char *Password, GDom *SettingStore, int Flags = 0); bool Close(); // Commands available while connected int GetMessages(); bool Receive(GArray &Trans, MailCallbacks *Callbacks = 0); bool Delete(int Message); int Sizeof(int Message); bool GetSizes(GArray &Sizes); bool GetUid(int Message, char *Id, int IdLen); bool GetUidList(List &Id); char *GetHeaders(int Message); }; class MailReceiveFolder : public MailSource { protected: class MailReceiveFolderPrivate *d; public: MailReceiveFolder(char *Path); ~MailReceiveFolder(); // Connection bool Open(GSocketI *S, const char *RemoteHost, int Port, const char *User, const char *Password, GDom *SettingStore, int Flags = 0); bool Close(); // Commands available while connected int GetMessages(); bool Receive(GArray &Trans, MailCallbacks *Callbacks = 0); bool Delete(int Message); int Sizeof(int Message); bool GetUid(int Message, char *Id, int IdLen); bool GetUidList(List &Id); char *GetHeaders(int Message); }; class MailPhp : public MailSource { protected: class MailPhpPrivate *d; bool Get(GSocketI *S, char *Uri, GStream &Out, bool ChopDot); public: MailPhp(); ~MailPhp(); // Connection bool Open(GSocketI *S, const char *RemoteHost, int Port, const char *User, const char *Password, GDom *SettingStore, int Flags = 0); bool Close(); // Commands available while connected int GetMessages(); bool Receive(GArray &Trans, MailCallbacks *Callbacks = 0); bool Delete(int Message); int Sizeof(int Message); bool GetSizes(GArray &Sizes); bool GetUid(int Message, char *Id, int IdLen); bool GetUidList(List &Id); char *GetHeaders(int Message); void SetProxy(char *Server, int Port); }; class MailImapFolder { friend class MailIMap; friend struct ImapThreadPrivate; char Sep; char *Path; public: bool NoSelect; bool NoInferiors; bool Marked; int Exists; int Recent; int Deleted; // int UnseenIndex; MailImapFolder(); virtual ~MailImapFolder(); char *GetPath(); void SetPath(const char *s); char *GetName(); void SetName(const char *s); char GetSep() { return Sep; } void operator =(LHashTbl,int> &v); }; class MailIMap : public MailSource { protected: class MailIMapPrivate *d; char Buf[2048]; List Uid; GStringPipe ReadBuf; List Dialog; void ClearDialog(); void ClearUid(); bool FillUidList(); bool WriteBuf(bool ObsurePass = false, const char *Buffer = 0, bool Continuation = false); bool ReadResponse(int Cmd = -1, bool Plus = false); bool Read(GStreamI *Out = 0, int Timeout = -1); bool ReadLine(); bool IsResponse(const char *Buf, int Cmd, bool &Ok); void CommandFinished(); public: typedef LHashTbl,GString> StrMap; struct StrRange { ssize_t Start, End; void Set(ssize_t s, ssize_t e) { Start = s; End = e; } ssize_t Len() { return End - Start; } }; // Typedefs struct Untagged { GString Cmd; GString Param; int Id; }; - struct OAuthParams - { - enum ServiceProvider - { - OAuthGoogle, - OAuthMicrosoft, - } Provider; - - GString ClientID; - GString ClientSecret; - GString RedirURIs; - GString AuthUri; - GString ApiUri; - // GString RevokeUri; - GString Scope; - GUri Proxy; - - GString AccessToken; - GString RefreshToken; - int ExpiresIn; - - bool IsValid() - { - return ClientID && - ClientSecret && - RedirURIs && - AuthUri && - // RevokeUri && - Scope && - ApiUri; - } - }; - /// This callback is used to notify the application using this object of IMAP fetch responses. /// \returns true if the application wants to continue reading and has taken ownership of the strings in "Parts". typedef bool (*FetchCallback) ( /// The IMAP object class MailIMap *Imap, /// The message sequence number char *Msg, /// The fetch parts (which the callee needs to own if returning true) StrMap &Parts, /// The user data passed to the Fetch function void *UserData ); // Object MailIMap(); ~MailIMap(); // Mutex bool Lock(const char *file, int line); bool LockWithTimeout(int Timeout, const char *file, int line); void Unlock(); // General char GetFolderSep(); char *EncodePath(const char *Path); char *GetCurrentPath(); bool GetExpungeOnExit(); void SetExpungeOnExit(bool b); bool ServerOption(char *Opt); bool IsOnline(); const char *GetWebLoginUri(); - void SetOAuthParams(OAuthParams &p); void SetParentWindow(GViewI *wnd); void SetCancel(LCancel *Cancel); ssize_t ParseImapResponse(char *Buffer, ssize_t BufferLen, GArray &Ranges, int Names); // Connection bool Open(GSocketI *S, const char *RemoteHost, int Port, const char *User, const char *Password, GDom *SettingStore, int Flags = 0); bool Close(); // Non-threadsafe soft close (normal operation) bool GetCapabilities(GArray &s); // Commands available while connected bool Receive(GArray &Trans, MailCallbacks *Callbacks = 0); int GetMessages(); bool Delete(int Message); bool Delete(bool ByUid, const char *Seq); int Sizeof(int Message); bool GetSizes(GArray &Sizes); bool GetUid(int Message, char *Id, int IdLen); bool GetUidList(List &Id); char *GetHeaders(int Message); char *SequenceToString(GArray *Seq); // Imap specific commands /// This method wraps the imap FETCH command /// \returns the number of records accepted by the callback fn int Fetch ( /// True if 'Seq' is a UID, otherwise it's a sequence bool ByUid, /// The sequence number or UID const char *Seq, /// The parts to retrieve const char *Parts, /// Data is returned to the caller via this callback function FetchCallback Callback, /// A user defined param to pass back to the 'Callback' function. void *UserData, /// [Optional] The raw data received will be written to this stream if provided, else NULL. GStreamI *RawCopy = 0, /// [Optional] The rough size of the fetch... used to pre-allocate a buffer to receive data. int64 SizeHint = -1 ); /// Appends a message to the specified folder bool Append ( /// The folder to write to const char *Folder, /// [Optional] Flags for the message ImapMailFlags *Flags, /// The rfc822 body of the message const char *Msg, /// [Out] The UID of the message appended (if known, can be empty if not known) GString &NewUid ); bool GetFolders(GArray &Folders); bool SelectFolder(const char *Path, StrMap *Values = 0); char *GetSelectedFolder(); int GetMessages(const char *Path); bool CreateFolder(MailImapFolder *f); bool DeleteFolder(const char *Path); bool RenameFolder(const char *From, const char *To); bool SetFolderFlags(MailImapFolder *f); /// Expunges (final delete) any deleted messages the current folder. bool ExpungeFolder(); // Uid methods bool CopyByUid(GArray &InUids, const char *DestFolder); bool SetFlagsByUid(GArray &Uids, const char *Flags); /// Idle processing... /// \returns true if something happened bool StartIdle(); bool OnIdle(int Timeout, GArray &Resp); bool FinishIdle(); bool Poll(int *Recent = 0, GArray *New = 0); bool Status(char *Path, int *Recent); bool Search(bool Uids, GArray &SeqNumbers, const char *Filter); // Utility static bool Http(GSocketI *S, GAutoString *OutHeaders, GAutoString *OutBody, int *StatusCode, const char *InMethod, const char *InUri, const char *InHeaders, const char *InBody); }; #endif diff --git a/src/common/Gdc2/Filters/Png.cpp b/src/common/Gdc2/Filters/Png.cpp --- a/src/common/Gdc2/Filters/Png.cpp +++ b/src/common/Gdc2/Filters/Png.cpp @@ -1,1548 +1,1546 @@ /*hdr ** FILE: Png.cpp ** AUTHOR: Matthew Allen ** DATE: 8/9/1998 ** DESCRIPTION: Png file filter ** ** Copyright (C) 2002, Matthew Allen ** fret@memecode.com */ // // 'png.h' comes from libpng, which you can get from: // http://www.libpng.org/pub/png/libpng.html // // You will also need zlib, which pnglib requires. zlib // is available here: // http://www.gzip.org/zlib // // If you don't want to build with PNG support then set // the define HAS_LIBPNG_ZLIB to '0' in Lgi.h // #ifndef __CYGWIN__ #include "math.h" #include "png.h" #endif #include "Lgi.h" #include "GPalette.h" #ifdef __CYGWIN__ #include "png.h" #endif #include #include #include #include "GLibraryUtils.h" #ifdef FILTER_UI #include "GTransparentDlg.h" #endif #include "GVariant.h" // Pixel formats typedef uint8_t Png8; typedef GRgb24 Png24; typedef GRgba32 Png32; typedef GRgb48 Png48; typedef GRgba64 Png64; #if PNG_LIBPNG_VER_MAJOR <= 1 && PNG_LIBPNG_VER_MINOR <= 2 #define png_const_infop png_infop #define png_const_bytep png_bytep #endif #if LIBPNG_SHARED #define LIBPNG Lib-> const char sLibrary[] = #if defined(MAC) "libpng15.15.4.0" #else #if defined(__CYGWIN__) "cygpng12" #else "libpng" #ifdef _MSC_VER _MSC_VER_STR #if defined(WIN64) "x64" #else "x32" #endif #endif #endif #endif ; // Library interface class LibPng : public GLibrary { public: LibPng() : GLibrary(sLibrary) { if (!IsLoaded()) { #if defined(MAC) if (!Load("/opt/local/lib/libpng.dylib")) #elif defined(LINUX) if (!Load("libpng16")) #endif { static bool First = true; if (First) { LgiTrace("%s:%i - Failed to load libpng.\n", _FL); First = false; } } } else { #if 0 char File[256]; GetModuleFileName(Handle(), File, sizeof(File)); LgiTrace("%s:%i - PNG: %s\n", _FL, File); #endif } } DynFunc4( png_structp, png_create_read_struct, png_const_charp, user_png_ver, png_voidp, error_ptr, png_error_ptr, error_fn, png_error_ptr, warn_fn); DynFunc4( png_structp, png_create_write_struct, png_const_charp, user_png_ver, png_voidp, error_ptr, png_error_ptr, error_fn, png_error_ptr, warn_fn); DynFunc1( png_infop, png_create_info_struct, png_structp, png_ptr); DynFunc2( int, png_destroy_info_struct, png_structp, png_ptr, png_infopp, info_ptr); DynFunc3( int, png_destroy_read_struct, png_structpp, png_ptr_ptr, png_infopp, info_ptr_ptr, png_infopp, end_info_ptr_ptr); DynFunc2( int, png_destroy_write_struct, png_structpp, png_ptr_ptr, png_infopp, info_ptr_ptr); DynFunc3( int, png_set_read_fn, png_structp, png_ptr, png_voidp, io_ptr, png_rw_ptr, read_data_fn); DynFunc4( int, png_set_write_fn, png_structp, png_ptr, png_voidp, io_ptr, png_rw_ptr, write_data_fn, png_flush_ptr, output_flush_fn); DynFunc4( int, png_read_png, png_structp, png_ptr, png_infop, info_ptr, int, transforms, png_voidp, params); DynFunc4( int, png_write_png, png_structp, png_ptr, png_infop, info_ptr, int, transforms, png_voidp, params); DynFunc2( png_bytepp, png_get_rows, png_structp, png_ptr, png_infop, info_ptr); DynFunc3( int, png_set_rows, png_structp, png_ptr, png_infop, info_ptr, png_bytepp, row_pointers); DynFunc6( png_uint_32, png_get_iCCP, png_structp, png_ptr, png_const_infop, info_ptr, png_charpp, name, int*, compression_type, png_bytepp, profile, png_uint_32*, proflen); DynFunc6( int, png_set_iCCP, png_structp, png_ptr, png_infop, info_ptr, png_charp, name, int, compression_type, png_const_bytep, profile, png_uint_32, proflen); DynFunc5( png_uint_32, png_get_tRNS, png_structp, png_ptr, png_infop, info_ptr, png_bytep*, trans_alpha, int*, num_trans, png_color_16p*, trans_color); DynFunc3( png_uint_32, png_get_valid, png_structp, png_ptr, png_infop, info_ptr, png_uint_32, flag); DynFunc4( png_uint_32, png_get_PLTE, png_structp, png_ptr, png_infop, info_ptr, png_colorp*, palette, int*, num_palette); DynFunc2( png_uint_32, png_get_image_width, png_structp, png_ptr, png_infop, info_ptr); DynFunc2( png_uint_32, png_get_image_height, png_structp, png_ptr, png_infop, info_ptr); DynFunc2( png_byte, png_get_channels, png_structp, png_ptr, png_infop, info_ptr); #if 1 // PNG_LIBPNG_VER <= 10250 DynFunc2( png_byte, png_get_color_type, png_structp, png_ptr, png_infop, info_ptr); #else DynFunc2( png_byte, png_get_color_type, png_const_structp, png_ptr, png_const_infop, info_ptr); #endif DynFunc2( png_byte, png_get_bit_depth, png_structp, png_ptr, png_infop, info_ptr); DynFunc1( png_voidp, png_get_error_ptr, png_structp, png_ptr); DynFunc1( png_voidp, png_get_io_ptr, png_structp, png_ptr); DynFunc9( int, png_set_IHDR, png_structp, png_ptr, png_infop, info_ptr, png_uint_32, width, png_uint_32, height, int, bit_depth, int, color_type, int, interlace_method, int, compression_method, int, filter_method); DynFunc4( int, png_set_PLTE, png_structp, png_ptr, png_infop, info_ptr, png_colorp, palette, int, num_palette); DynFunc5( int, png_set_tRNS, png_structp, png_ptr, png_infop, info_ptr, png_bytep, trans_alpha, int, num_trans, png_color_16p, trans_color); /* DynFunc2( png_byte, png_get_interlace_type, png_const_structp, png_ptr, png_const_infop, info_ptr); */ }; class InitLibPng : public LMutex { GAutoPtr Png; public: LibPng *Get() { if (Lock(_FL)) { if (!Png) Png.Reset(new LibPng); Unlock(); } return Png; } } CurrentLibPng; #else #define LIBPNG #endif class GdcPng : public GFilter { static char PngSig[]; friend void PNGAPI LibPngError(png_structp Png, png_const_charp Msg); friend void PNGAPI LibPngWarning(png_structp Png, png_const_charp Msg); #if LIBPNG_SHARED LibPng *Lib; #endif int Pos; uchar *PrevScanLine; GSurface *pDC; GMemQueue DataPipe; GView *Parent; jmp_buf Here; public: GdcPng ( #if LIBPNG_SHARED LibPng *lib #endif ); ~GdcPng(); const char *GetComponentName() { return "libpng"; } Format GetFormat() { return FmtPng; } void SetMeter(int i) { if (Meter) Meter->Value(i); } int GetCapabilites() { return FILTER_CAP_READ | FILTER_CAP_WRITE; } IoStatus ReadImage(GSurface *pDC, GStream *In); IoStatus WriteImage(GStream *Out, GSurface *pDC); bool GetVariant(const char *n, GVariant &v, char *a) { if (!_stricmp(n, LGI_FILTER_TYPE)) { v = "Png"; // Portable Network Graphic } else if (!_stricmp(n, LGI_FILTER_EXTENSIONS)) { v = "PNG"; } else return false; return true; } }; // Object Factory class GdcPngFactory : public GFilterFactory { bool CheckFile(const char *File, int Access, const uchar *Hint) { if (Hint) { return Hint[1] == 'P' && Hint[2] == 'N' && Hint[3] == 'G'; } else { return (File) ? stristr(File, ".png") != 0 : false; } } GFilter *NewObject() { return new GdcPng ( #if LIBPNG_SHARED CurrentLibPng.Get() #endif ); } } PngFactory; // Class impl char GdcPng::PngSig[] = { (char)137, 'P', 'N', 'G', '\r', '\n', (char)26, '\n', 0 }; GdcPng::GdcPng( #if LIBPNG_SHARED LibPng *lib #endif ) { #if LIBPNG_SHARED Lib = lib; #endif Parent = 0; Pos = 0; PrevScanLine = 0; } GdcPng::~GdcPng() { DeleteArray(PrevScanLine); } void PNGAPI LibPngError(png_structp Png, png_const_charp Msg) { GdcPng *This = (GdcPng*) #if LIBPNG_SHARED CurrentLibPng.Get()-> #endif png_get_error_ptr(Png); if (This) { printf("Libpng Error Message='%s'\n", Msg); if (This->Props) { GVariant v; This->Props->SetValue(LGI_FILTER_ERROR, v = (char*)Msg); } longjmp(This->Here, -1); } } void PNGAPI LibPngWarning(png_structp Png, png_const_charp Msg) { LgiTrace("LibPng Warning: %s\n", Msg); } void PNGAPI LibPngRead(png_structp Png, png_bytep Ptr, png_size_t Size) { GStream *s = (GStream*) #if LIBPNG_SHARED CurrentLibPng.Get()-> #endif png_get_io_ptr(Png); if (s) { s->Read(Ptr, Size); } else { LgiTrace("%s:%i - No this ptr? (%p)\n", __FILE__, __LINE__, Ptr); LgiAssert(0); } } struct PngWriteInfo { GStream *s; Progress *m; }; void PNGAPI LibPngWrite(png_structp Png, png_bytep Ptr, png_size_t Size) { PngWriteInfo *i = (PngWriteInfo*) #if LIBPNG_SHARED CurrentLibPng.Get()-> #endif png_get_io_ptr(Png); if (i) { i->s->Write(Ptr, Size); /* if (i->m) i->m->Value(Png->flush_rows); */ } else { LgiTrace("%s:%i - No this ptr?\n", __FILE__, __LINE__); LgiAssert(0); } } template void Read32_16(Out *o, In *i, int Len) { In *e = i + Len; while (i < e) { o->r = i->r >> 3; o->g = i->g >> 2; o->b = i->b >> 3; o++; i++; } } template void Read64_16(Out *o, In *i, int Len) { In *e = i + Len; while (i < e) { o->r = i->r >> 11; o->g = i->g >> 10; o->b = i->b >> 11; o++; i++; } } template void Read32_24(Out *o, In *i, int Len) { In *e = i + Len; while (i < e) { o->r = i->r; o->g = i->g; o->b = i->b; o++; i++; } } template void Read64_24(Out *o, In *i, int Len) { In *e = i + Len; while (i < e) { o->r = i->r >> 8; o->g = i->g >> 8; o->b = i->b >> 8; o++; i++; } } template void Read32_32(Out *o, In *i, int Len) { In *e = i + Len; while (i < e) { o->r = i->r; o->g = i->g; o->b = i->b; o->a = 255; o++; i++; } } template void Read64_32(Out *o, In *i, int Len) { In *e = i + Len; while (i < e) { o->r = i->r >> 8; o->g = i->g >> 8; o->b = i->b >> 8; o->a = 255; o++; i++; } } template void ReadAlpha32_16(Out *o, In *i, int Len) { In *e = i + Len; while (i < e) { o->r = i->r >> 3; o->g = i->g >> 2; o->b = i->b >> 3; o++; i++; } } template void ReadAlpha64_16(Out *o, In *i, int Len) { In *e = i + Len; while (i < e) { o->r = i->r >> 11; o->g = i->g >> 10; o->b = i->b >> 11; o++; i++; } } template void ReadAlpha32_24(Out *o, In *i, int Len) { In *e = i + Len; while (i < e) { o->r = i->r; o->g = i->g; o->b = i->b; o++; i++; } } template void ReadAlpha64_24(Out *o, In *i, int Len) { In *e = i + Len; while (i < e) { o->r = i->r >> 8; o->g = i->g >> 8; o->b = i->b >> 8; o++; i++; } } template void ReadAlpha32_32(Out *o, In *i, int Len) { In *e = i + Len; while (i < e) { o->r = i->r; o->g = i->g; o->b = i->b; o->a = i->a; o++; i++; } } template void ReadAlpha64_32(Out *o, In *i, int Len) { In *e = i + Len; while (i < e) { o->r = i->r >> 8; o->g = i->g >> 8; o->b = i->b >> 8; o->a = i->a >> 8; o++; i++; } } GFilter::IoStatus GdcPng::ReadImage(GSurface *pDeviceContext, GStream *In) { GFilter::IoStatus Status = IoError; Pos = 0; pDC = pDeviceContext; DeleteArray(PrevScanLine); GVariant v; if (Props && Props->GetValue(LGI_FILTER_PARENT_WND, v) && v.Type == GV_GVIEW) { Parent = (GView*)v.Value.Ptr; } #if LIBPNG_SHARED if (!Lib->IsLoaded() && !Lib->Load(sLibrary)) { GString s; s.Printf("libpng is missing (%s.%s)", sLibrary, LGI_LIBRARY_EXT); if (Props) Props->SetValue(LGI_FILTER_ERROR, v = s); else LgiTrace("%s:%i - %s\n", _FL, s.Get()); static bool Warn = true; if (Warn) { LgiTrace("%s:%i - Unable to load libpng (%s.%s).\n", _FL, sLibrary, LGI_LIBRARY_EXT); Warn = false; } return GFilter::IoComponentMissing; } #endif png_structp png_ptr = NULL; if (setjmp(Here)) { return Status; } png_ptr = LIBPNG png_create_read_struct( PNG_LIBPNG_VER_STRING, (void*)this, LibPngError, LibPngWarning); if (!png_ptr) { if (Props) Props->SetValue(LGI_FILTER_ERROR, v = "png_create_read_struct failed."); } else { png_infop info_ptr = LIBPNG png_create_info_struct(png_ptr); if (info_ptr) { LIBPNG png_set_read_fn(png_ptr, In, LibPngRead); #if 0 // What was this for again? int off = (char*)&png_ptr->io_ptr - (char*)png_ptr; if (!png_ptr->io_ptr) { printf("io_ptr offset = %i\n", off); LgiAssert(0); CurrentLibPng = 0; return false; } #endif LIBPNG png_read_png(png_ptr, info_ptr, 0, 0); png_bytepp Scan0 = LIBPNG png_get_rows(png_ptr, info_ptr); if (Scan0) { int BitDepth = LIBPNG png_get_bit_depth(png_ptr, info_ptr); int FinalBits = BitDepth == 16 ? 8 : BitDepth; int ColourType = LIBPNG png_get_color_type(png_ptr, info_ptr); int Channels = LIBPNG png_get_channels(png_ptr, info_ptr); int RequestBits = FinalBits * Channels; GColourSpace InCs = ColourType == PNG_COLOR_TYPE_GRAY_ALPHA ? CsIndex8 : GBitsToColourSpace(MAX(RequestBits, 8)); if (!pDC->Create( LIBPNG png_get_image_width(png_ptr, info_ptr), LIBPNG png_get_image_height(png_ptr, info_ptr), InCs, GSurface::SurfaceRequireExactCs)) { printf("%s:%i - GMemDC::Create(%i, %i, %i) failed.\n", _FL, LIBPNG png_get_image_width(png_ptr, info_ptr), LIBPNG png_get_image_height(png_ptr, info_ptr), RequestBits); } else { bool Error = false; #if 1 if (ColourType == PNG_COLOR_TYPE_GRAY_ALPHA) { pDC->HasAlpha(true); // Setup alpha channel } /* printf("PngRead %s->%s\n", GColourSpaceToString(InCs), GColourSpaceToString(pDC->GetColourSpace())); */ #endif // Copy in the scanlines int ActualBits = pDC->GetBits(); int ScanLen = LIBPNG png_get_image_width(png_ptr, info_ptr) * ActualBits / 8; GColourSpace OutCs = pDC->GetColourSpace(); for (int y=0; yY() && !Error; y++) { uchar *Scan = (*pDC)[y]; LgiAssert(Scan != NULL); switch (RequestBits) { case 1: { uchar *o = Scan; uchar *e = Scan + pDC->X(); uchar *i = Scan0[y]; uchar Mask = 0x80; while (o < e) { *o++ = (*i & Mask) ? 1 : 0; Mask >>= 1; if (!Mask) { i++; Mask = 0x80; } } break; } case 4: { uchar *i = Scan0[y]; uchar *o = Scan; for (int x=0; xX(); x++) { if (x & 1) *o++ = *i++ & 0xf; else *o++ = (*i >> 4) & 0xf; } break; } case 8: { memcpy(Scan, Scan0[y], ScanLen); break; } case 16: { if (ColourType == PNG_COLOR_TYPE_GRAY_ALPHA) { uint8_t *grey = Scan; uint8_t *alpha = (*(pDC->AlphaDC()))[y]; LgiAssert(grey && alpha); uint8_t *end = grey + pDC->X(); uint8_t *in = Scan0[y]; while (grey < end) { *grey++ = *in++; *alpha++ = *in++; } } else { memcpy(Scan, Scan0[y], ScanLen); } break; } case 24: { switch (OutCs) { #define Read24Case(name, bits) \ case Cs##name: \ { \ if (LIBPNG png_get_bit_depth(png_ptr, info_ptr) == 16) \ Read64_##bits((G##name*)Scan, (Png48*)Scan0[y], pDC->X()); \ else \ Read32_##bits((G##name*)Scan, (Png24*)Scan0[y], pDC->X()); \ break; \ } Read24Case(Rgb16, 16); Read24Case(Bgr16, 16); Read24Case(Rgb24, 24); Read24Case(Bgr24, 24); Read24Case(Xrgb32, 24); Read24Case(Rgbx32, 24); Read24Case(Xbgr32, 24); Read24Case(Bgrx32, 24); Read24Case(Rgba32, 32); Read24Case(Bgra32, 32); Read24Case(Argb32, 32); Read24Case(Abgr32, 32); default: LgiTrace("%s:%i - Unsupported colour space: 0x%x (%s)\n", _FL, pDC->GetColourSpace(), GColourSpaceToString(pDC->GetColourSpace())); LgiAssert(!"Not impl."); break; } break; } case 32: { switch (pDC->GetColourSpace()) { #define Read32Case(name, bits) \ case Cs##name: \ { \ if (LIBPNG png_get_bit_depth(png_ptr, info_ptr) == 16) \ ReadAlpha64_##bits((G##name*)Scan, (Png64*)Scan0[y], pDC->X()); \ else \ ReadAlpha32_##bits((G##name*)Scan, (Png32*)Scan0[y], pDC->X()); \ break; \ } Read32Case(Rgb16, 16); Read32Case(Bgr16, 16); Read32Case(Rgb24, 24); Read32Case(Bgr24, 24); Read32Case(Xrgb32, 24); Read32Case(Rgbx32, 24); Read32Case(Xbgr32, 24); Read32Case(Bgrx32, 24); Read32Case(Rgba32, 32); Read32Case(Bgra32, 32); Read32Case(Argb32, 32); Read32Case(Abgr32, 32); default: LgiTrace("%s:%i - Unsupported colour space: 0x%x (%s)\n", _FL, pDC->GetColourSpace(), GColourSpaceToString(pDC->GetColourSpace())); LgiAssert(!"Not impl."); if (Props) Props->SetValue(LGI_FILTER_ERROR, v = "Missing scan convertor"); Error = true; break; } break; } default: { if (ActualBits == RequestBits) { memcpy(Scan, Scan0[y], ScanLen); } else { LgiAssert(!"Yeah you need to impl a convertor here."); if (Props) Props->SetValue(LGI_FILTER_ERROR, v = "Missing scan convertor"); Error = true; } break; } } } if (RequestBits == 32) { // bool IsPreMul = pDC->IsPreMultipliedAlpha(); pDC->ConvertPreMulAlpha(true); } if (ActualBits <= 8) { // Copy in the palette png_colorp pal; int num_pal = 0; if (LIBPNG png_get_PLTE(png_ptr, info_ptr, &pal, &num_pal) == PNG_INFO_PLTE) { GPalette *Pal = new GPalette(0, num_pal); if (Pal) { for (int i=0; ir = pal[i].red; Rgb->g = pal[i].green; Rgb->b = pal[i].blue; } } pDC->Palette(Pal, true); } } if (LIBPNG png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS)) { png_bytep trans_alpha = NULL; png_color_16p trans_color; int num_trans; if (LIBPNG png_get_tRNS(png_ptr, info_ptr, &trans_alpha, &num_trans, &trans_color)) { pDC->HasAlpha(true); GSurface *Alpha = pDC->AlphaDC(); if (Alpha) { if (trans_alpha) { for (int y=0; yY(); y++) { uchar *a = (*Alpha)[y]; uchar *p = (*pDC)[y]; for (int x=0; xX(); x++) { if (p[x] < num_trans) { a[x] = trans_alpha[p[x]]; } else { a[x] = 0xff; } } } } else if (trans_color) { for (int y=0; yY(); y++) { uchar *a = (*Alpha)[y]; uchar *p = (*pDC)[y]; for (int x=0; xX(); x++) { a[x] = p[x] == trans_color->index ? 0x00 : 0xff; } } } } else { printf("%s:%i - No alpha channel.\n", _FL); } } else { printf("%s:%i - Bad trans ptr.\n", _FL); } } } Status = Error ? IoError : IoSuccess; } } else LgiTrace("%s:%i - png_get_rows failed.\n", _FL); LIBPNG png_destroy_info_struct(png_ptr, &info_ptr); } png_charp ProfName = 0; int CompressionType = 0; png_bytep ColProf = 0; png_uint_32 ColProfLen = 0; if (LIBPNG png_get_iCCP(png_ptr, info_ptr, &ProfName, &CompressionType, &ColProf, &ColProfLen) && Props) { v.SetBinary(ColProfLen, ColProf); Props->SetValue(LGI_FILTER_COLOUR_PROF, v); } LIBPNG png_destroy_read_struct(&png_ptr, 0, 0); } return Status; } GFilter::IoStatus GdcPng::WriteImage(GStream *Out, GSurface *pDC) { GFilter::IoStatus Status = IoError; GVariant Transparent; bool HasTransparency = false; COLOUR Back = 0; GVariant v; if (!pDC) return GFilter::IoError; #if LIBPNG_SHARED if (!Lib->IsLoaded() && !Lib->Load(sLibrary)) { static bool Warn = true; if (Warn) { LgiTrace("%s:%i - Unabled to load libpng.\n", _FL); Warn = false; } return GFilter::IoComponentMissing; } #endif // Work out whether the image has transparency if (pDC->GetBits() == 32) { // Check alpha channel for (int y=0; yY() && !HasTransparency; y++) { System32BitPixel *p = (System32BitPixel*)(*pDC)[y]; if (!p) break; System32BitPixel *e = p + pDC->X(); while (p < e) { if (p->a < 255) { HasTransparency = true; break; } p++; } } } else if (pDC->AlphaDC()) { GSurface *a = pDC->AlphaDC(); if (a) { for (int y=0; yY() && !HasTransparency; y++) { uint8_t *p = (*a)[y]; if (!p) break; uint8_t *e = p + a->X(); while (p < e) { if (*p < 255) { HasTransparency = true; break; } p++; } } } } if (Props) { if (Props->GetValue(LGI_FILTER_PARENT_WND, v) && v.Type == GV_GVIEW) { Parent = (GView*)v.Value.Ptr; } if (Props->GetValue(LGI_FILTER_BACKGROUND, v)) { Back = v.CastInt32(); } Props->GetValue(LGI_FILTER_TRANSPARENT, Transparent); } #ifdef FILTER_UI if (Parent && Transparent.IsNull()) { // put up a dialog to ask about transparent colour GTransparentDlg Dlg(Parent, &Transparent); if (!Dlg.DoModal()) { if (Props) Props->SetValue("Cancel", v = 1); return IoCancel; } } #endif if (setjmp(Here) == 0 && pDC && Out) { GVariant ColProfile; if (Props) { Props->GetValue(LGI_FILTER_COLOUR_PROF, ColProfile); } // setup png_structp png_ptr = LIBPNG png_create_write_struct( PNG_LIBPNG_VER_STRING, (void*)this, LibPngError, LibPngWarning); if (png_ptr) { png_infop info_ptr = LIBPNG png_create_info_struct(png_ptr); if (info_ptr) { Out->SetSize(0); PngWriteInfo WriteInfo; WriteInfo.s = Out; WriteInfo.m = Meter; LIBPNG png_set_write_fn(png_ptr, &WriteInfo, LibPngWrite, 0); // png_set_write_status_fn(png_ptr, write_row_callback); bool KeyAlpha = false; bool ChannelAlpha = false; GMemDC *pTemp = 0; if (pDC->AlphaDC() && HasTransparency) { pTemp = new GMemDC(pDC->X(), pDC->Y(), System32BitColourSpace); if (pTemp) { pTemp->Colour(0); pTemp->Rectangle(); pTemp->Op(GDC_ALPHA); pTemp->Blt(0, 0, pDC); pTemp->Op(GDC_SET); pDC = pTemp; ChannelAlpha = true; } } else { if (Transparent.CastInt32() && Props && Props->GetValue(LGI_FILTER_BACKGROUND, v)) { KeyAlpha = true; } } int Ar = R32(Back); int Ag = G32(Back); int Ab = B32(Back); if (pDC->GetBits() == 32) { if (!ChannelAlpha && !KeyAlpha) { for (int y=0; yY(); y++) { System32BitPixel *s = (System32BitPixel*) (*pDC)[y]; for (int x=0; xX(); x++) { if (s[x].a < 0xff) { ChannelAlpha = true; y = pDC->Y(); break; } } } } } bool ExtraAlphaChannel = ChannelAlpha || (pDC->GetBits() > 8 ? KeyAlpha : 0); int ColourType; if (pDC->GetBits() <= 8) { if (pDC->Palette()) ColourType = PNG_COLOR_TYPE_PALETTE; else ColourType = PNG_COLOR_TYPE_GRAY; } else if (ExtraAlphaChannel) { ColourType = PNG_COLOR_TYPE_RGB_ALPHA; } else { ColourType = PNG_COLOR_TYPE_RGB; } LIBPNG png_set_IHDR(png_ptr, info_ptr, pDC->X(), pDC->Y(), 8, ColourType, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); if (ColProfile.Type == GV_BINARY) { LIBPNG png_set_iCCP(png_ptr, info_ptr, (png_charp)"ColourProfile", NULL, (png_const_bytep) ColProfile.Value.Binary.Data, (png_uint_32) ColProfile.Value.Binary.Length); } int TempLine = pDC->X() * ((pDC->GetBits() <= 8 ? 1 : 3) + (ExtraAlphaChannel ? 1 : 0)); uchar *TempBits = new uchar[pDC->Y() * TempLine]; if (Meter) Meter->SetLimits(0, pDC->Y()); switch (pDC->GetBits()) { case 8: { // Output the palette GPalette *Pal = pDC->Palette(); if (Pal) { int Colours = Pal->GetSize(); GAutoPtr PngPal(new png_color[Colours]); if (PngPal) { for (int i=0; ir; PngPal[i].green = Rgb->g; PngPal[i].blue = Rgb->b; } } LIBPNG png_set_PLTE(png_ptr, info_ptr, PngPal, Colours); } } // Copy the pixels for (int y=0; yY(); y++) { uchar *s = (*pDC)[y]; Png8 *d = (Png8*) (TempBits + (TempLine * y)); for (int x=0; xX(); x++) { *d++ = *s++; } } // Setup the transparent palette entry if (KeyAlpha) { static png_byte Trans[256]; for (uint n=0; nbit_depth = 8; //info_ptr->channels = 3 + (ExtraAlphaChannel ? 1 : 0); //info_ptr->color_type = PNG_COLOR_TYPE_RGB | (KeyAlpha ? PNG_COLOR_MASK_ALPHA : 0); for (int y=0; yY(); y++) { uint16 *s = (uint16*) (*pDC)[y]; if (pDC->GetBits() == 15) { if (KeyAlpha) { Png32 *d = (Png32*) (TempBits + (TempLine * y)); for (int x=0; xX(); x++) { d->r = Rc15(*s); d->g = Gc15(*s); d->b = Bc15(*s); d->a = (d->r == Ar && d->g == Ag && d->b == Ab) ? 0 : 0xff; s++; d++; } } else { Png24 *d = (Png24*) (TempBits + (TempLine * y)); for (int x=0; xX(); x++) { d->r = Rc15(*s); d->g = Gc15(*s); d->b = Bc15(*s); s++; d++; } } } else { if (KeyAlpha) { Png32 *d = (Png32*) (TempBits + (TempLine * y)); for (int x=0; xX(); x++) { d->r = Rc16(*s); d->g = Gc16(*s); d->b = Bc16(*s); d->a = (d->r == Ar && d->g == Ag && d->b == Ab) ? 0 : 0xff; s++; d++; } } else { Png24 *d = (Png24*) (TempBits + (TempLine * y)); for (int x=0; xX(); x++) { d->r = Rc16(*s); d->g = Gc16(*s); d->b = Bc16(*s); s++; d++; } } } } break; } case 24: { //info_ptr->bit_depth = 8; //info_ptr->channels = 3 + (KeyAlpha ? 1 : 0); //info_ptr->color_type = PNG_COLOR_TYPE_RGB | (KeyAlpha ? PNG_COLOR_MASK_ALPHA : 0); for (int y=0; yY(); y++) { System24BitPixel *s = (System24BitPixel*) (*pDC)[y]; if (KeyAlpha) { Png32 *d = (Png32*) (TempBits + (TempLine * y)); for (int x=0; xX(); x++) { d->r = s->r; d->g = s->g; d->b = s->b; d->a = (s->r == Ar && s->g == Ag && s->b == Ab) ? 0 : 0xff; s++; d++; } } else { Png24 *d = (Png24*) (TempBits + (TempLine * y)); for (int x=0; xX(); x++) { d->r = s->r; d->g = s->g; d->b = s->b; s++; d++; } } } break; } case 32: { //info_ptr->bit_depth = 8; //info_ptr->channels = 3 + (ExtraAlphaChannel ? 1 : 0); //info_ptr->color_type = PNG_COLOR_TYPE_RGB | (ExtraAlphaChannel ? PNG_COLOR_MASK_ALPHA : 0); for (int y=0; yY(); y++) { System32BitPixel *s = (System32BitPixel*) (*pDC)[y]; if (ChannelAlpha) { Png32 *d = (Png32*) (TempBits + (TempLine * y)); for (int x=0; xX(); x++) { d->r = s->r; d->g = s->g; d->b = s->b; d->a = s->a; s++; d++; } } else if (KeyAlpha) { Png32 *d = (Png32*) (TempBits + (TempLine * y)); Png32 *e = d + pDC->X(); while (d < e) { if (s->a == 0 || (s->r == Ar && s->g == Ag && s->b == Ab) ) { d->r = 0; d->g = 0; d->b = 0; d->a = 0; } else { d->r = s->r; d->g = s->g; d->b = s->b; d->a = s->a; } s++; d++; } } else { Png24 *d = (Png24*) (TempBits + (TempLine * y)); for (int x=0; xX(); x++) { d->r = s->r; d->g = s->g; d->b = s->b; s++; d++; } } } break; } default: { goto CleanUp; } } - png_bytep *row = new png_bytep[pDC->Y()]; - if (row) + GArray row; + if (row.Length(pDC->Y())) { for (int y=0; yY(); y++) { row[y] = TempBits + (TempLine * y); } - LIBPNG png_set_rows(png_ptr, info_ptr, row); + LIBPNG png_set_rows(png_ptr, info_ptr, row.AddressOf()); LIBPNG png_write_png(png_ptr, info_ptr, 0, 0); Status = IoSuccess; - - DeleteArray(row); } DeleteArray(TempBits); DeleteObj(pTemp); LIBPNG png_destroy_info_struct(png_ptr, &info_ptr); } CleanUp: LIBPNG png_destroy_write_struct(&png_ptr, NULL); } } return Status; } diff --git a/src/common/General/LDateTime.cpp b/src/common/General/LDateTime.cpp --- a/src/common/General/LDateTime.cpp +++ b/src/common/General/LDateTime.cpp @@ -1,2059 +1,2086 @@ /* ** FILE: LDateTime.cpp ** AUTHOR: Matthew Allen ** DATE: 11/11/98 ** DESCRIPTION: Scribe Date Time Object ** ** Copyright (C) 1998, Matthew Allen ** fret@memecode.com */ #define _INTEGRAL_MAX_BITS 64 #include #include #include #include #include #ifdef MAC #include #endif #ifdef WINDOWS #include #endif #include "Lgi.h" #include "LDateTime.h" #include "GToken.h" #include "GDocView.h" ////////////////////////////////////////////////////////////////////////////// uint16 LDateTime::DefaultFormat = GDTF_DEFAULT; char LDateTime::DefaultSeparator = '/'; uint16 LDateTime::GetDefaultFormat() { if (DefaultFormat == GDTF_DEFAULT) { #ifdef WIN32 TCHAR s[80] = _T("1"); GetLocaleInfo(LOCALE_USER_DEFAULT, LOCALE_IDATE, s, CountOf(s)); switch (_tstoi(s)) { case 0: DefaultFormat = GDTF_MONTH_DAY_YEAR; break; default: case 1: DefaultFormat = GDTF_DAY_MONTH_YEAR; break; case 2: DefaultFormat = GDTF_YEAR_MONTH_DAY; break; } GetLocaleInfo(LOCALE_USER_DEFAULT, LOCALE_ITIME, s, sizeof(s)); if (_tstoi(s) == 1) { DefaultFormat |= GDTF_24HOUR; } else { DefaultFormat |= GDTF_12HOUR; } if (GetLocaleInfo(LOCALE_USER_DEFAULT, LOCALE_SDATE, s, sizeof(s))) DefaultSeparator = (char)s[0]; if (GetLocaleInfo(LOCALE_USER_DEFAULT, LOCALE_SSHORTDATE, s, sizeof(s))) { char Sep[] = { DefaultSeparator, '/', '\\', '-', '.', 0 }; GString Str = s; GToken t(Str, Sep); for (int i=0; i= low && (v) <= high) bool LDateTime::IsValid() { return InRange(_Day, 1, 31) && InRange(_Year, 1600, 2100) && InRange(_Thousands, 0, 999) && InRange(_Month, 1, 12) && InRange(_Seconds, 0, 59) && InRange(_Minutes, 0, 59) && InRange(_Hours, 0, 23) && InRange(_Tz, -720, 720); } void LDateTime::SetTimeZone(int NewTz, bool ConvertTime) { if (ConvertTime && NewTz != _Tz) { // printf("SetTimeZone: %i\n", NewTz - _Tz); AddMinutes(NewTz - _Tz); } _Tz = NewTz; } int LDateTime::SystemTimeZone(bool ForceUpdate) { if (ForceUpdate || CurTz == NO_ZONE) { CurTz = 0; CurTzOff = 0; #ifdef MAC #if 0 //def COCOA LgiAssert(!"Fixme"); #else CFTimeZoneRef tz = CFTimeZoneCopySystem(); CFAbsoluteTime now = CFAbsoluteTimeGetCurrent(); Boolean dst = CFTimeZoneIsDaylightSavingTime(tz, now); if (dst) { CFAbsoluteTime next = CFTimeZoneGetNextDaylightSavingTimeTransition(tz, now); CurTz = CFTimeZoneGetSecondsFromGMT(tz, next + 100) / 60; } else { CurTz = CFTimeZoneGetSecondsFromGMT(tz, now) / 60; } CurTzOff = CFTimeZoneGetDaylightSavingTimeOffset(tz, now) / 60; CFRelease(tz); #endif #elif defined(WIN32) timeb tbTime; ftime(&tbTime); CurTz = -tbTime.timezone; TIME_ZONE_INFORMATION Tzi; if (GetTimeZoneInformation(&Tzi) == TIME_ZONE_ID_DAYLIGHT) CurTzOff = -Tzi.DaylightBias; #elif defined(LINUX) int six_months = (365 * 24 * 60 * 60) / 2; time_t now = 0, then = 0; time (&now); then = now - six_months; tm now_tz, then_tz; tm *t = localtime_r(&now, &now_tz); if (t) { localtime_r(&then, &then_tz); CurTz = now_tz.tm_gmtoff / 60; if (now_tz.tm_isdst) { CurTzOff = (now_tz.tm_gmtoff - then_tz.tm_gmtoff) / 60; CurTz = then_tz.tm_gmtoff / 60; } else CurTzOff = (then_tz.tm_gmtoff - now_tz.tm_gmtoff) / 60; } else return NO_ZONE; #else #error "Impl me." #endif } return CurTz + CurTzOff; } int LDateTime::SystemTimeZoneOffset() { if (CurTz == NO_ZONE) SystemTimeZone(); return CurTzOff; } #if defined WIN32 LDateTime ConvertSysTime(SYSTEMTIME &st, int year) { LDateTime n; if (st.wYear) { n.Year(st.wYear); n.Month(st.wMonth); n.Day(st.wDay); } else { n.Year(year); n.Month(st.wMonth); // Find the 'nth' matching weekday, starting from the first day in the month n.Day(1); LDateTime c = n; for (int i=0; iCompare(b); } #elif defined POSIX static bool ParseValue(char *s, GAutoString &var, GAutoString &val) { if (!s) return false; char *e = strchr(s, '='); if (!e) return false; *e++ = 0; var.Reset(NewStr(s)); val.Reset(NewStr(e)); *e = '='; return var != 0 && val != 0; } #endif /* Testing code... LDateTime Start, End; GArray Info; Start.Set("1/1/2010"); End.Set("31/12/2014"); LDateTime::GetDaylightSavingsInfo(Info, Start, &End); GStringPipe p; for (int i=0; i,int> { MonthHash() { Add("Jan", 1); Add("Feb", 2); Add("Mar", 3); Add("Apr", 4); Add("May", 5); Add("Jun", 6); Add("Jul", 7); Add("Aug", 8); Add("Sep", 9); Add("Oct", 10); Add("Nov", 11); Add("Dec", 12); Add("January", 1); Add("February", 2); Add("March", 3); Add("April", 4); Add("May", 5); Add("June", 6); Add("July", 7); Add("August", 8); Add("September", 9); Add("October", 10); Add("November", 11); Add("December", 12); } }; GString::Array Zdump; bool LDateTime::GetDaylightSavingsInfo(GArray &Info, LDateTime &Start, LDateTime *End) { bool Status = false; #if defined(WIN32) TIME_ZONE_INFORMATION Tzi; if (GetTimeZoneInformation(&Tzi) == TIME_ZONE_ID_DAYLIGHT) { Info.Length(0); // Find the DST->Normal date in the same year as Start LDateTime n = ConvertSysTime(Tzi.StandardDate, Start.Year()); // Find the Normal->DST date in the same year as Start LDateTime d = ConvertSysTime(Tzi.DaylightDate, Start.Year()); // Create initial Info entry Info[0].UtcTimeStamp = Start; bool IsDst = (n < d) ^ !(Start < n || Start > d); if (IsDst) // Start is DST Info[0].Offset = -(Tzi.Bias + Tzi.DaylightBias); else // Start is normal Info[0].Offset = -(Tzi.Bias + Tzi.StandardBias); if (End) { // Build list of DST change dates GArray c; c.Add(n); c.Add(d); for (int y = Start.Year() + 1; y <= End->Year(); y++) { // Calculate the dates for the following years if required c.Add(ConvertSysTime(Tzi.StandardDate, y)); c.Add(ConvertSysTime(Tzi.DaylightDate, y)); } c.Sort(GDateCmp); // Itererate over the list to generate further Info entries for (int i=0; i Start && dt < *End) { IsDst = !IsDst; GDstInfo &inf = Info.New(); if (IsDst) inf.Offset = -(Tzi.Bias + Tzi.DaylightBias); else inf.Offset = -(Tzi.Bias + Tzi.StandardBias); dt.SetTimeZone(inf.Offset, false); dt.SetTimeZone(0, true); inf.UtcTimeStamp = dt; } } } Status = true; } #elif defined(MAC) || defined(LINUX) if (!Zdump.Length()) { FILE *f = popen("zdump -v /etc/localtime", "r"); if (f) { char s[256]; size_t r; GStringPipe p(1024); while ((r = fread(s, 1, sizeof(s), f)) > 0) { p.Write(s, (int)r); } fclose(f); GString ps = p.NewGStr(); Zdump = ps.Split("\n"); } } MonthHash Lut; LDateTime Prev; int PrevOff = 0; for (int i=0; i= 16 && !stricmp(l[0], "/etc/localtime")) { // /etc/localtime Sat Oct 3 15:59:59 2037 UTC = Sun Oct 4 01:59:59 2037 EST isdst=0 gmtoff=36000 // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 LDateTime Utc; Utc.Year(atoi(l[5])); GToken Tm(l[4], ":"); if (Tm.Length() == 3) { Utc.Hours(atoi(Tm[0])); Utc.Minutes(atoi(Tm[1])); Utc.Seconds(atoi(Tm[2])); if (Utc.Minutes() == 0) { int m = Lut.Find(l[2]); if (m) { Utc.Day(atoi(l[3])); Utc.Month(m); GAutoString Var, Val; if (ParseValue(l[14], Var, Val) && !stricmp(Var, "isdst")) { // int IsDst = atoi(Val); if (ParseValue(l[15], Var, Val) && !stricmp(Var, "gmtoff")) { int Off = atoi(Val) / 60; if (Prev.Year() && Prev < Start && Start < Utc) { /* char Tmp[64]; Utc.Get(Tmp, sizeof(Tmp)); printf("[%i] Utc=%s\n", Info.Length(), Tmp); Prev.Get(Tmp, sizeof(Tmp)); printf("[%i] Prev=%s\n", Info.Length(), Tmp); Start.Get(Tmp, sizeof(Tmp)); printf("[%i] Start=%s\n", Info.Length(), Tmp); */ // Emit initial entry for 'start' Info[0].UtcTimeStamp = Start; Info[0].Offset = PrevOff; Status = true; } if (Utc > Start && End && Utc < *End) { // Emit furthur entries for DST events between start and end. GDstInfo &inf = Info.New(); inf.UtcTimeStamp = Utc; inf.Offset = Off; } Prev = Utc; PrevOff = Off; } else printf("%s:%i - Unknown value for isdst\n", _FL); } else printf("%s:%i - Unknown value for isdst\n", _FL); } else printf("%s:%i - Unknown month '%s'\n", _FL, l[2]); } // else printf("%s:%i - UTC min wrong %s.\n", _FL, l[4]); } else printf("%s:%i - Tm '%s' has wrong parts: %s\n", _FL, l[4], Line); } } #elif defined BEOS #else LgiAssert(!"Not implemented."); #endif return Status; } int LDateTime::DayOfWeek() { int Index = 0; int Day = IsLeapYear() ? 29 : 28; switch (_Year / 100) { case 19: { Index = 3; break; } case 20: { Index = 2; break; } } // get year right int y = _Year % 100; int r = y % 12; Index = (Index + (y / 12) + r + (r / 4)) % 7; // get month right if (_Month % 2 == 0) { // even month if (_Month > 2) Day = _Month; } else { // odd month switch (_Month) { case 1: { Day = 31; if (IsLeapYear()) { Index = Index > 0 ? Index - 1 : Index + 6; } break; } case 11: case 3: { Day = 7; break; } case 5: { Day = 9; break; } case 7: { Day = 11; break; } case 9: { Day = 5; break; } } } // get day right int Diff = Index - (Day - _Day); while (Diff < 0) Diff += 7; return Diff % 7; } void LDateTime::SetNow() { #ifdef WIN32 SYSTEMTIME stNow; FILETIME ftNow; GetSystemTime(&stNow); SystemTimeToFileTime(&stNow, &ftNow); uint64 i64 = ((uint64)ftNow.dwHighDateTime << 32) | ftNow.dwLowDateTime; Set(i64); #else time_t now; time(&now); struct tm *time = localtime(&now); if (time) *this = time; #ifndef LGI_STATIC else { LgiTrace("%s:%i - Error: localtime failed, now=%u\n", _FL, now); } #endif #endif } #define Convert24HrTo12Hr(h) ( (h) == 0 ? 12 : (h) > 12 ? (h) % 12 : (h) ) #define Convert24HrToAmPm(h) ( (h) >= 12 ? "p" : "a" ) GString LDateTime::GetDate() { char s[32]; int Ch = GetDate(s, sizeof(s)); return GString(s, Ch); } int LDateTime::GetDate(char *Str, size_t SLen) { int Ch = 0; if (Str && SLen > 0) { switch (_Format & GDTF_DATE_MASK) { case GDTF_MONTH_DAY_YEAR: Ch += sprintf_s(Str+Ch, SLen-Ch, _Format&GDTF_MONTH_LEADINGZ?"%2.2i" :"%i" , _Month); Ch += sprintf_s(Str+Ch, SLen-Ch, _Format&GDTF_DAY_LEADINGZ ?"%c%2.2i":"%c%i", DefaultSeparator, _Day); Ch += sprintf_s(Str+Ch, SLen-Ch, "%c%i", DefaultSeparator, _Year); break; default: case GDTF_DAY_MONTH_YEAR: Ch += sprintf_s(Str+Ch, SLen-Ch, _Format&GDTF_DAY_LEADINGZ ?"%2.2i" :"%i" , _Day); Ch += sprintf_s(Str+Ch, SLen-Ch, _Format&GDTF_MONTH_LEADINGZ?"%c%2.2i":"%c%i", DefaultSeparator, _Month); Ch += sprintf_s(Str+Ch, SLen-Ch, "%c%i", DefaultSeparator, _Year); break; case GDTF_YEAR_MONTH_DAY: Ch += sprintf_s(Str+Ch, SLen-Ch, "%i", _Year); Ch += sprintf_s(Str+Ch, SLen-Ch, _Format&GDTF_MONTH_LEADINGZ?"%c%2.2i":"%c%i", DefaultSeparator, _Month); Ch += sprintf_s(Str+Ch, SLen-Ch, _Format&GDTF_DAY_LEADINGZ ?"%c%2.2i":"%c%i", DefaultSeparator, _Day); break; } } return Ch; } GString LDateTime::GetTime() { char s[32]; int Ch = GetTime(s, sizeof(s)); return GString(s, Ch); } int LDateTime::GetTime(char *Str, size_t SLen) { int Ch = 0; if (Str && SLen > 0) { switch (_Format & GDTF_TIME_MASK) { case GDTF_12HOUR: default: { Ch += sprintf_s(Str, SLen, "%i:%2.2i:%2.2i%s", Convert24HrTo12Hr(_Hours), _Minutes, _Seconds, Convert24HrToAmPm(_Hours)); break; } case GDTF_24HOUR: { Ch += sprintf_s(Str, SLen, "%i:%2.2i:%2.2i", _Hours, _Minutes, _Seconds); break; } } } return Ch; } uint64 LDateTime::Ts() { uint64 ts; Get(ts); return ts; } +bool LDateTime::SetUnix(uint64 s) +{ + #if defined(WINDOWS) + return Set(s * LDateTime::Second64Bit + 116445168000000000LL); + #else + return Set(s); + #endif +} + bool LDateTime::Set(uint64 s) { #if defined WIN32 FILETIME Utc; SYSTEMTIME System; // Adjust to the desired timezone uint64 u = s + ((int64)_Tz * 60 * Second64Bit); Utc.dwHighDateTime = u >> 32; Utc.dwLowDateTime = u & 0xffffffff; if (FileTimeToSystemTime(&Utc, &System)) { _Year = System.wYear; _Month = System.wMonth; _Day = System.wDay; _Hours = System.wHour; _Minutes = System.wMinute; _Seconds = System.wSecond; _Thousands = System.wMilliseconds; return true; } return false; #else Set((time_t)(s / Second64Bit)); _Thousands = s % Second64Bit; return true; #endif } bool LDateTime::Set(time_t tt) { struct tm *t; #if !defined(_MSC_VER) || _MSC_VER < _MSC_VER_VS2005 t = localtime(&tt); if (t) #else struct tm tmp; if (_localtime64_s(t = &tmp, &tt) == 0) #endif { _Year = t->tm_year + 1900; _Month = t->tm_mon + 1; _Day = t->tm_mday; _Hours = t->tm_hour; _Minutes = t->tm_min; _Seconds = t->tm_sec; _Thousands = 0; _Tz = SystemTimeZone(); return true; } return false; } bool LDateTime::Get(uint64 &s) { #ifdef WIN32 FILETIME Utc; SYSTEMTIME System; System.wYear = _Year; System.wMonth = limit(_Month, 1, 12); System.wDay = limit(_Day, 1, 31); System.wHour = limit(_Hours, 0, 23); System.wMinute = limit(_Minutes, 0, 59); System.wSecond = limit(_Seconds, 0, 59); System.wMilliseconds = limit(_Thousands, 0, 999); System.wDayOfWeek = DayOfWeek(); BOOL b1; if (b1 = SystemTimeToFileTime(&System, &Utc)) { // Convert to 64bit s = ((uint64)Utc.dwHighDateTime << 32) | Utc.dwLowDateTime; // Adjust for timezone s -= (int64)_Tz * 60 * Second64Bit; return true; } DWORD Err = GetLastError(); s = 0; LgiAssert(!"SystemTimeToFileTime failed."); return false; #else struct tm t; ZeroObj(t); t.tm_year = _Year - 1900; t.tm_mon = _Month - 1; t.tm_mday = _Day; t.tm_hour = _Hours; t.tm_min = _Minutes; t.tm_sec = _Seconds; t.tm_isdst = -1; /* mktime assumes input is in localtime. This is fine if CurTz == _Tz but if it's different we need to adjust the output to give the correct value. */ time_t sec = mktime(&t); if (sec == -1) return false; /* int CurTz = SystemTimeZone(); if (CurTz != _Tz) { // Adjust the output to the correct time zone.. int Diff = _Tz - CurTz; sec += Diff * 60; printf("Adjusting += %i (%i -> %i)\n", Diff * 60, CurTz, _Tz); } else printf("No Adjusting\n"); */ s = (uint64)sec * Second64Bit + _Thousands; return true; #endif } GString LDateTime::Get() { char buf[32]; int Ch = GetDate(buf, sizeof(buf)); buf[Ch++] = ' '; Ch += GetTime(buf+Ch, sizeof(buf)-Ch); return GString(buf, Ch); } void LDateTime::Get(char *Str, size_t SLen) { if (Str) { GetDate(Str, SLen); size_t len = strlen(Str); if (len < SLen - 1) { Str[len++] = ' '; GetTime(Str+len, SLen-len); } } } bool LDateTime::Set(const char *Str) { bool Status = false; if (Str) { char Local[256]; strcpy_s(Local, sizeof(Local), Str); char *Sep = strchr(Local, ' '); if (Sep) { *Sep++ = 0; Status |= SetTime(Sep); } Status |= SetDate(Local); } return Status; } void LDateTime::Month(char *m) { int i = IsMonth(m); if (i >= 0) _Month = i + 1; } bool LDateTime::SetDate(const char *Str) { bool Status = false; if (Str) { GToken T(Str, "/-.,_\\"); if (T.Length() == 3) { switch (_Format & GDTF_DATE_MASK) { case GDTF_MONTH_DAY_YEAR: { _Month = atoi(T[0]); _Day = atoi(T[1]); _Year = atoi(T[2]); break; } case GDTF_DAY_MONTH_YEAR: { _Day = atoi(T[0]); _Month = atoi(T[1]); _Year = atoi(T[2]); break; } case GDTF_YEAR_MONTH_DAY: { _Year = atoi(T[0]); _Month = atoi(T[1]); _Day = atoi(T[2]); break; } default: { int n[3] = { atoi(T[0]), atoi(T[1]), atoi(T[2]) }; if (n[0] > 1000) { // yyyy/m/d _Year = n[0]; _Month = n[1]; _Day = n[2]; } else if (n[2] > 1000) { _Year = n[2]; if (n[0] > 12) { // d/m/yyyy _Day = n[0]; _Month = n[1]; } else if (n[1] > 12) { // m/d/yyyy _Day = n[1]; _Month = n[0]; } else if ((DefaultFormat & GDTF_DATE_MASK) == GDTF_MONTH_DAY_YEAR) { // Assume m/d/yyyy _Day = n[1]; _Month = n[0]; } else { // Who knows??? // Assume d/m/yyyy _Day = n[0]; _Month = n[1]; } } break; } } if (_Year < 100) { if (_Year >= 80) { _Year += 1900; } else { _Year += 2000; } } Status = true; } else { // Fall back to fuzzy matching GToken T(Str, " ,"); MonthHash Lut; int FMonth = 0; int FDay = 0; int FYear = 0; for (unsigned i=0; i 0) { if (i >= 1000) { FYear = i; } else if (i < 32) { FDay = i; } } } else { int i = Lut.Find(p); if (i) FMonth = i; } } if (FMonth && FDay) { Day(FDay); Month(FMonth); } if (FYear) { Year(FYear); } else { LDateTime Now; Now.SetNow(); Year(Now.Year()); } } } return Status; } bool LDateTime::SetTime(const char *Str) { bool Status = false; if (Str) { GToken T(Str, ":."); if (T.Length() >= 2 && T.Length() <= 4) { _Hours = atoi(T[0]); _Minutes = atoi(T[1]); char *s = T[2]; if (s) _Seconds = atoi(s); else _Seconds = 0; s = T[T.Length()-1]; if (s) { if (strchr(s, 'p') || strchr(s, 'P')) { if (_Hours != 12) { _Hours += 12; } } else if (strchr(s, 'a') || strchr(s, 'A')) { if (_Hours == 12) { _Hours -= 12; } } } _Thousands = (T.Length() > 3) ? atoi(T[3]) : 0; Status = true; } } return Status; } int LDateTime::IsWeekDay(const char *s) { static const char *Short[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; static const char *Long[] = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}; for (unsigned n=0; n= 4) { Year((int)t[0].Int()); Month((int)t[1].Int()); Day((int)t[2].Int()); } else if (t[2].Length() >= 4) { Day((int)t[0].Int()); Month((int)t[1].Int()); Year((int)t[2].Int()); } else { LgiAssert(!"Unknown date format?"); return false; } } } else if (a[i].Length() == 4) Year((int)a[i].Int()); else if (!Day()) Day((int)a[i].Int()); } else if (IsAlpha(*c)) { int WkDay = IsWeekDay(c); if (WkDay >= 0) continue; int Mnth = IsMonth(c); if (Mnth >= 0) Month(Mnth + 1); } else if (*c == '-' || *c == '+') { c++; if (strlen(c) == 4) { // Timezone.. int64 Tz = a[i].Int(); int Hrs = (int) (Tz / 100); int Min = (int) (Tz % 100); SetTimeZone(Hrs * 60 + Min, false); } } } return IsValid(); } int LDateTime::Sizeof() { return sizeof(int) * 7; } bool LDateTime::Serialize(GFile &f, bool Write) { int32 i; if (Write) { #define wf(fld) i = fld; f << i; wf(_Day); wf(_Month); wf(_Year); wf(_Thousands); wf(_Seconds); wf(_Minutes); wf(_Hours); } else { #define rf(fld) f >> i; fld = i; rf(_Day); rf(_Month); rf(_Year); rf(_Thousands); rf(_Seconds); rf(_Minutes); rf(_Hours); } return true; } /* bool LDateTime::Serialize(ObjProperties *Props, char *Name, bool Write) { #ifndef LGI_STATIC if (Props && Name) { struct _Date { uint8 Day; uint8 Month; int16 Year; uint8 Hour; uint8 Minute; uint16 ThouSec; }; LgiAssert(sizeof(_Date) == 8); if (Write) { _Date d; d.Day = _Day; d.Month = _Month; d.Year = _Year; d.Hour = _Hours; d.Minute = _Minutes; d.ThouSec = (_Seconds * 1000) + _Thousands; return Props->Set(Name, &d, sizeof(d)); } else // Read { void *Ptr; int Len; if (Props->Get(Name, Ptr, Len) && sizeof(_Date) == Len) { _Date *d = (_Date*) Ptr; _Day = d->Day; _Month = d->Month; _Year = d->Year; _Hours = d->Hour; _Minutes = d->Minute; _Seconds = d->ThouSec / 1000; _Thousands = d->ThouSec % 1000; return true; } } } #endif return false; } */ int LDateTime::Compare(const LDateTime *d) const { int c = 0; if (d) { c = _Year - d->_Year; if (!c) { c = _Month - d->_Month; if (!c) { c = _Day - d->_Day; if (!c) { c = _Hours - d->_Hours; if (!c) { c = _Minutes - d->_Minutes; if (!c) { c = _Seconds - d->_Seconds; if (!c) { c = _Thousands - d->_Thousands; } } } } } } } return c; } bool LDateTime::operator <(LDateTime &dt) const { if (_Year < dt._Year) return true; else if (_Year > dt._Year) return false; if (_Month < dt._Month) return true; else if (_Month > dt._Month) return false; if (_Day < dt._Day) return true; else if (_Day > dt._Day) return false; if (_Hours < dt._Hours) return true; else if (_Hours > dt._Hours) return false; if (_Minutes < dt._Minutes) return true; else if (_Minutes > dt._Minutes) return false; if (_Seconds < dt._Seconds) return true; else if (_Seconds > dt._Seconds) return false; if (_Thousands < dt._Thousands) return true; else if (_Thousands > dt._Thousands) return false; return false; } bool LDateTime::operator <=(LDateTime &dt) const { return !(*this > dt); } bool LDateTime::operator >(LDateTime &dt) const { if (_Year > dt._Year) return true; else if (_Year < dt._Year) return false; if (_Month > dt._Month) return true; else if (_Month < dt._Month) return false; if (_Day > dt._Day) return true; else if (_Day < dt._Day) return false; if (_Hours > dt._Hours) return true; else if (_Hours < dt._Hours) return false; if (_Minutes > dt._Minutes) return true; else if (_Minutes < dt._Minutes) return false; if (_Seconds > dt._Seconds) return true; else if (_Seconds < dt._Seconds) return false; if (_Thousands > dt._Thousands) return true; else if (_Thousands < dt._Thousands) return false; return false; } bool LDateTime::operator >=(LDateTime &dt) const { return !(*this < dt); } bool LDateTime::operator ==(const LDateTime &dt) const { return _Year == dt._Year && _Month == dt._Month && _Day == dt._Day && _Hours == dt._Hours && _Minutes == dt._Minutes && _Seconds == dt._Seconds && _Thousands == dt._Thousands; } bool LDateTime::operator !=(LDateTime &dt) const { return _Year != dt._Year || _Month != dt._Month || _Day != dt._Day || _Hours != dt._Hours || _Minutes != dt._Minutes || _Seconds != dt._Seconds || _Thousands != dt._Thousands; } int LDateTime::DiffMonths(LDateTime &dt) { int a = (Year() * 12) + Month(); int b = (dt.Year() * 12) + dt.Month(); return b - a; } LDateTime LDateTime::operator -(LDateTime &dt) { uint64 a, b; Get(a); dt.Get(b); /// Resolution of a second when using 64 bit timestamps int64 Sec = Second64Bit; int64 Min = 60 * Sec; int64 Hr = 60 * Min; int64 Day = 24 * Hr; int64 d = (int64)a - (int64)b; LDateTime r; r._Day = (int16) (d / Day); d -= r._Day * Day; r._Hours = (int16) (d / Hr); d -= r._Hours * Hr; r._Minutes = (int16) (d / Min); d -= r._Minutes * Min; r._Seconds = (int16) (d / Sec); #ifdef WIN32 d -= r._Seconds * Sec; r._Thousands = (int16) (d / 10000); #else r._Thousands = 0; #endif return r; } LDateTime LDateTime::operator +(LDateTime &dt) { LDateTime s = *this; s.AddMonths(dt.Month()); s.AddDays(dt.Day()); s.AddHours(dt.Hours()); s.AddMinutes(dt.Minutes()); // s.AddSeconds(dt.Seconds()); return s; } LDateTime &LDateTime::operator =(const LDateTime &t) { _Day = t._Day; _Year = t._Year; _Thousands = t._Thousands; _Month = t._Month; _Seconds = t._Seconds; _Minutes = t._Minutes; _Hours = t._Hours; _Tz = t._Tz; _Format = t._Format; return *this; } LDateTime &LDateTime::operator =(struct tm *time) { if (time) { _Seconds = time->tm_sec; _Minutes = time->tm_min; _Hours = time->tm_hour; _Day = time->tm_mday; _Month = time->tm_mon + 1; _Year = time->tm_year + 1900; } else Empty(); return *this; } bool LDateTime::IsSameDay(LDateTime &d) { return Day() == d.Day() && Month() == d.Month() && Year() == d.Year(); } bool LDateTime::IsSameMonth(LDateTime &d) { return Day() == d.Day() && Month() == d.Month(); } bool LDateTime::IsSameYear(LDateTime &d) { return Year() == d.Year(); } bool LDateTime::IsLeapYear(int Year) { if (Year < 0) Year = _Year; if (Year % 4 != 0) { return false; } if (Year % 400 == 0) { return true; } if (Year % 100 == 0) { return false; } return true; } int LDateTime::DaysInMonth() { if (_Month == 2 && IsLeapYear()) { return 29; } short DaysInMonth[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; return _Month >= 1 && _Month <= 12 ? DaysInMonth[_Month-1] : 0; } #define MinutesInDay (60*24) void LDateTime::AddSeconds(int64 Seconds) { uint64 i; if (Get(i)) { i += Seconds * Second64Bit; Set(i); } } void LDateTime::AddMinutes(int64 Minutes) { uint64 i; if (Get(i)) { int64 delta = Minutes * 60 * Second64Bit; uint64 n = i + delta; // printf("AddMin " LPrintfInt64 " + " LPrintfInt64 " = " LPrintfInt64 "\n", i, delta, n); Set(n); } } void LDateTime::AddHours(int64 Hours) { uint64 i; if (Get(i)) { i += Hours * 3600 * Second64Bit; Set(i); } } bool LDateTime::AddDays(int64 Days) { if (!Days) return true; uint64 Ts; if (!Get(Ts)) return false; uint64 DayTicks = (uint64)LDateTime::Second64Bit * 60 * 60 * 24; Ts += Days * DayTicks; bool b = Set(Ts); return b; } void LDateTime::AddMonths(int64 Months) { int64 m = _Month + Months; do { if (m < 1) { _Year--; m += 12; } else if (m > 12) { _Year++; m -= 12; } else { break; } } while (1); _Month = (int16) m; if (_Day > DaysInMonth()) _Day = DaysInMonth(); } int LDateTime::MonthFromName(const char *Name) { if (Name) { const char *MonthName[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; for (int m=0; m<12; m++) { if (strnicmp(Name, MonthName[m], strlen(MonthName[m])) == 0) { return m + 1; break; } } } return -1; } bool LDateTime::Decode(const char *In) { // Test data: // // Tue, 6 Dec 2005 1:25:32 -0800 Empty(); if (!In) { LgiAssert(0); return false; } bool Status = false; // Tokenize delimited by whitespace GString::Array T = GString(In).SplitDelimit(", \t\r\n"); - if (T.Length() >= 2) + if (T.Length() < 2) + { + if (T[0].IsNumeric()) + { + // Some sort of timestamp? + uint64_t Ts = Atoi(T[0].Get()); + if (Ts > 0) + { + return SetUnix(Ts); + } + else return false; + } + else + { + // What now? + return false; + } + } + else { bool GotDate = false; for (unsigned i=0; i 31) { // Y/M/D? Year((int)Date[0].Int()); Day((int)Date[2].Int()); } else if (Date[2].Int() > 31) { // D/M/Y? Day((int)Date[0].Int()); Year((int)Date[2].Int()); } else { // Ambiguous year... bool YrFirst = true; if (Date[0].Length() == 1) YrFirst = false; // else we really can't tell.. just go with year first if (YrFirst) { Year((int)Date[0].Int()); Day((int)Date[2].Int()); } else { Day((int)Date[0].Int()); Year((int)Date[2].Int()); } LDateTime Now; Now.SetNow(); if (Year() + 2000 <= Now.Year()) Year(2000 + Year()); else Year(1900 + Year()); } if (Date[1].IsNumeric()) Month((int)Date[1].Int()); else { int m = MonthFromName(Date[1]); if (m > 0) Month(m); } GotDate = true; Status = true; } else if (s.Find(":") >= 0) { // whole time // Do some validation bool Valid = true; for (char *c = s; *c && Valid; c++) { if (!(IsDigit(*c) || *c == ':')) Valid = false; } if (Valid) { GString::Array Time = s.Split(":"); if (Time.Length() == 2 || Time.Length() == 3) { // Hour int i = (int) Time[0].Int(); if (i >= 0) Hours(i); if (s.Lower().Find("p") >= 0) { if (Hours() < 12) Hours(Hours() + 12); } // Minute i = (int) Time[1].Int(); if (i >= 0) Minutes(i); if (Time.Length() == 3) { // Second i = (int) Time[2].Int(); if (i >= 0) Seconds(i); } Status = true; } } } else if (IsAlpha(s(0))) { // text int m = MonthFromName(s); if (m > 0) Month(m); } else if (strchr("+-", *s)) { // timezone DoTimeZone: LDateTime Now; double OurTmz = (double)Now.SystemTimeZone() / 60; if (s && strchr("-+", *s) && strlen(s) == 5) { #if 1 int i = atoi(s); int hr = i / 100; int min = i % 100; SetTimeZone(hr * 60 + min, false); #else // adjust for timezone char Buf[32]; memcpy(Buf, s, 3); Buf[3] = 0; double TheirTmz = atof(Buf); memcpy(Buf+1, s + 3, 2); TheirTmz += (atof(Buf) / 60); if (Tz) { *Tz = TheirTmz; } double AdjustHours = OurTmz - TheirTmz; AddMinutes((int) (AdjustHours * 60)); #endif } else { // assume GMT AddMinutes((int) (OurTmz * 60)); } } else if (s.IsNumeric()) { int Count = 0; for (char *c = s; *c; c++) { if (!IsDigit(*c)) break; Count++; } if (Count <= 2) { if (Day()) { // We already have a day... so this might be // a 2 digit year... LDateTime Now; Now.SetNow(); int Yr = atoi(s); if (2000 + Yr <= Now.Year()) Year(2000 + Yr); else Year(1900 + Yr); } else { // A day number (hopefully)? Day((int)s.Int()); } } else if (Count == 4) { if (!Year()) { // A year! Year((int)s.Int()); Status = true; } else { goto DoTimeZone; } // My one and only Y2K fix // d.Year((Yr < 100) ? (Yr > 50) ? 1900+Yr : 2000+Yr : Yr); } } } } return Status; } bool LDateTime::GetVariant(const char *Name, GVariant &Dst, char *Array) { GDomProperty p = LgiStringToDomProp(Name); switch (p) { case DateYear: // Type: Int32 Dst = Year(); break; case DateMonth: // Type: Int32 Dst = Month(); break; case DateDay: // Type: Int32 Dst = Day(); break; case DateHour: // Type: Int32 Dst = Hours(); break; case DateMinute: // Type: Int32 Dst = Minutes(); break; case DateSecond: // Type: Int32 Dst = Seconds(); break; case DateDate: // Type: String { char s[32]; GetDate(s, sizeof(s)); Dst = s; break; } case DateTime: // Type: String { char s[32]; GetTime(s, sizeof(s)); Dst = s; break; } case DateDateAndTime: // Type: String { char s[32]; Get(s, sizeof(s)); Dst = s; break; } case DateTimestamp: // Type: Int64 { uint64 i = 0; Get(i); Dst = (int64)i; break; } default: { return false; } } return true; } bool LDateTime::SetVariant(const char *Name, GVariant &Value, char *Array) { GDomProperty p = LgiStringToDomProp(Name); switch (p) { case DateYear: Year(Value.CastInt32()); break; case DateMonth: Month(Value.CastInt32()); break; case DateDay: Day(Value.CastInt32()); break; case DateHour: Hours(Value.CastInt32()); break; case DateMinute: Minutes(Value.CastInt32()); break; case DateSecond: Seconds(Value.CastInt32()); break; case DateDate: SetDate(Value.Str()); break; case DateTime: SetTime(Value.Str()); break; case DateDateAndTime: Set(Value.Str()); break; case DateTimestamp: Set((uint64)Value.CastInt64()); break; default: return false; } return true; } bool LDateTime::CallMethod(const char *Name, GVariant *ReturnValue, GArray &Args) { switch (LgiStringToDomProp(Name)) { case DateSetNow: SetNow(); if (ReturnValue) *ReturnValue = true; break; case DateSetStr: if (Args.Length() < 1) return false; bool Status; if (Args[0]->Type == GV_INT64) Status = Set((uint64) Args[0]->Value.Int64); else Status = Set(Args[0]->Str()); if (ReturnValue) *ReturnValue = Status; break; case DateGetStr: { char s[256] = ""; Get(s, sizeof(s)); if (ReturnValue) *ReturnValue = s; break; } default: return false; } return true; } #ifdef _DEBUG #define DATE_ASSERT(i) \ if (!(i)) \ { \ LgiAssert(!"LDateTime unit test failed."); \ return false; \ } bool LDateTime_Test() { // Check 64bit get/set LDateTime t("1/1/2017 0:0:0"); uint64 i; DATE_ASSERT(t.Get(i)); LgiTrace("Get='%s'\n", t.Get().Get()); uint64 i2 = i + (24ULL * 60 * 60 * LDateTime::Second64Bit); LDateTime t2; t2.SetFormat(GDTF_DAY_MONTH_YEAR); t2.Set(i2); GString s = t2.Get(); LgiTrace("Set='%s'\n", s.Get()); DATE_ASSERT(!stricmp(s, "2/1/2017 12:00:00a") || !stricmp(s, "2/01/2017 12:00:00a")); t.SetNow(); LgiTrace("Now.Local=%s Tz=%.2f\n", t.Get().Get(), t.GetTimeZoneHours()); t2 = t; t2.ToUtc(); LgiTrace("Now.Utc=%s Tz=%.2f\n", t2.Get().Get(), t2.GetTimeZoneHours()); t2.ToLocal(); LgiTrace("Now.Local=%s Tz=%.2f\n", t2.Get().Get(), t2.GetTimeZoneHours()); DATE_ASSERT(t == t2); return true; } #endif diff --git a/src/common/INet/LOAuth2.cpp b/src/common/INet/LOAuth2.cpp new file mode 100644 --- /dev/null +++ b/src/common/INet/LOAuth2.cpp @@ -0,0 +1,429 @@ +#include "Lgi.h" +#include "resdefs.h" +#include "GTextLog.h" +#include "OpenSSLSocket.h" +#include "Base64.h" +#include "INetTools.h" +#include "LOAuth2.h" +#include "LJSon.h" + +////////////////////////////////////////////////////////////////// +#define LOCALHOST_PORT 54900 +#define OPT_AccessToken "AccessToken" +#define OPT_RefreshToken "RefreshToken" + +static GString GetHeaders(GSocketI *s) +{ + char Buf[256]; + ssize_t Rd; + GString p; + while ((Rd = s->Read(Buf, sizeof(Buf))) > 0) + { + p += GString(Buf, Rd); + if (p.Find("\r\n\r\n") >= 0) + return p; + } + + s->Close(); + return NULL; +} + +ssize_t ChunkSize(ssize_t &Pos, GString &Buf, GString &Body) +{ + static GString Eol("\r\n"); + auto End = Buf.Find(Eol, Pos); + if (End > Pos) + { + auto Sz = Buf(Pos, End).Int(16); + if (Sz >= 0) + { + End += Eol.Length(); + + auto Bytes = End + Sz + Eol.Length(); + if (Buf.Length() >= Bytes) + { + Body += Buf(End, End + Sz); + Pos = End + Sz + Eol.Length(); + return Sz; + } + } + } + + return -1; +} + +static bool GetHttp(GSocketI *s, GString &Hdrs, GString &Body, bool IsResponse) +{ + GString Resp = GetHeaders(s); + + char Buf[512]; + ssize_t Rd; + auto BodyPos = Resp.Find("\r\n\r\n"); + GAutoString Len(InetGetHeaderField(Resp, "Content-Length", BodyPos)); + if (Len) + { + int Bytes = atoi(Len); + size_t Total = BodyPos + 4 + Bytes; + while (Resp.Length() < Total) + { + Rd = s->Read(Buf, sizeof(Buf)); + if (Rd > 0) + { + Resp += GString(Buf, Rd); + } + } + } + else if (s->IsOpen() && IsResponse) + { + GAutoString Te(InetGetHeaderField(Resp, "Transfer-Encoding", BodyPos)); + bool Chunked = Te && !_stricmp(Te, "chunked"); + + if (Chunked) + { + ssize_t Pos = 0; + + Hdrs = Resp(0, BodyPos); + GString Raw = Resp(BodyPos + 4, -1); + Body.Empty(); + + while (s->IsOpen()) + { + auto Sz = ChunkSize(Pos, Raw, Body); + if (Sz == 0) + break; + if (Sz < 0) + { + Rd = s->Read(Buf, sizeof(Buf)); + if (Rd > 0) + Raw += GString(Buf, Rd); + else + break; + } + } + + return true; + } + else + { + while ((Rd = s->Read(Buf, sizeof(Buf))) > 0) + Resp += GString(Buf, Rd); + } + } + + Hdrs = Resp(0, BodyPos); + Body = Resp(BodyPos + 4, -1); + + return true; +} + +static GString UrlFromHeaders(GString Hdrs) +{ + auto Lines = Hdrs.Split("\r\n", 1); + auto p = Lines[0].SplitDelimit(); + if (p.Length() < 3) + { + return NULL; + } + + return p[1]; +} + +static bool Write(GSocketI *s, GString b) +{ + for (size_t i = 0; i < b.Length(); ) + { + auto Wr = s->Write(b.Get() + i, b.Length() - i); + if (Wr <= 0) + return false; + i += Wr; + } + return true; +} + +static GString FormEncode(const char *s, bool InValue = true) +{ + GStringPipe p; + for (auto c = s; *c; c++) + { + if (isalpha(*c) || isdigit(*c) || *c == '_' || *c == '.' || (!InValue && *c == '+') || *c == '-' || *c == '%') + { + p.Write(c, 1); + } + else if (*c == ' ') + { + p.Write((char*)"+", 1); + } + else + { + p.Print("%%%02.2X", *c); + } + } + return p.NewGStr(); +} + +struct LOAuth2Priv : public LCancel +{ + LOAuth2::Params Params; + GString Id; + GStream *Log; + GString Token; + GString CodeVerifier; + GStringPipe LocalLog; + GDom *Store; + + GString AccessToken, RefreshToken; + int64 ExpiresIn; + + struct Server : public GSocket + { + GSocket Listen; + LOAuth2Priv *d; + GSocket s; + + public: + LHashTbl,GString> Params; + GString Body; + + Server(LOAuth2Priv *cd) : d(cd) + { + while (!Listen.Listen(LOCALHOST_PORT)) + { + if (d->IsCancelled()) + break; + d->Log->Print("Error: Can't listen on %i...\n", LOCALHOST_PORT); + LgiSleep(1000); + } + } + + bool GetReq() + { + while (!d->IsCancelled()) + { + if (Listen.IsReadable(100)) + { + if (Listen.Accept(&s)) + { + // Read access code out of response + GString Hdrs; + if (GetHttp(&s, Hdrs, Body, false)) + { + auto Url = UrlFromHeaders(Hdrs); + auto Vars = Url.Split("?", 1); + if (Vars.Length() != 2) + { + return false; + } + + Vars = Vars[1].Split("&"); + for (auto v : Vars) + { + auto p = v.Split("=", 1); + if (p.Length() != 2) + continue; + Params.Add(p[0], p[1]); + } + + return true; + } + } + } + } + + return false; + } + + bool Response(const char *Txt) + { + GString Msg; + Msg.Printf("HTTP/1.0 200 OK\r\n" + "\r\n" + "\n" + "%s\n" + "", + Txt); + return ::Write(&s, Msg); + } + }; + + GString Base64(GString s) + { + GString b; + b.Length(BufferLen_BinTo64(s.Length())); + auto ch = ConvertBinaryToBase64(b.Get(), b.Length(), (uchar*)s.Get(), s.Length()); + b.Get()[b.Length()] = 0; + return b; + } + + GString ToText(GString Bin) + { + GArray t; + for (char i='0'; i<='9'; i++) t.Add(i); + for (char i='a'; i<='z'; i++) t.Add(i); + for (char i='A'; i<='Z'; i++) t.Add(i); + t.Add('-'); t.Add('.'); t.Add('_'); t.Add('~'); + GString Txt; + Txt.Length(Bin.Length()); + int Pos = 0; + for (int i=0; iPrint("Error: Can't connect to '%s:%i'\n", u.Host, HTTPS_PORT); + return NULL; + } + + GString Body, Http; + Body.Printf("code=%s&" + "client_id=%s&" + "client_secret=%s&" + "redirect_uri=http://localhost:%i&" + "code_verifier=%s&" + "grant_type=authorization_code", + FormEncode(Token).Get(), + Params.ClientID.Get(), + Params.ClientSecret.Get(), + LOCALHOST_PORT, + FormEncode(CodeVerifier).Get()); + + Http.Printf("POST /oauth2/v4/token HTTP/1.1\r\n" + "Host: www.googleapis.com\r\n" + "Content-Type: application/x-www-form-urlencoded\r\n" + "Content-length: " LPrintfSizeT "\r\n" + "\r\n" + "%s", + Body.Length(), + Body.Get()); + if (!Write(&sock, Http)) + { + Log->Print("%s:%i - Error writing to socket.\n", _FL); + return false; + } + + GString Hdrs; + if (!GetHttp(&sock, Hdrs, Body, true)) + { + return false; + } + + // Log->Print("Body=%s\n", Body.Get()); + LJson j(Body); + + AccessToken = j.Get("access_token"); + RefreshToken = j.Get("refresh_token"); + ExpiresIn = j.Get("expires_in").Int(); + } + + return AccessToken.Get() != NULL; + } + + LOAuth2Priv(LOAuth2::Params ¶ms, const char *account, GDom *store, GStream *log) + { + Params = params; + Id = account; + Store = store; + Log = log ? log : &LocalLog; + } + + bool Serialize(bool Write) + { + if (!Store) + return false; + + GVariant v; + GString Key, kAccTok; + Key.Printf("%s.%s", Params.Scope.Get(), Id.Get()); + auto KeyB64 = Base64(Key); + kAccTok.Printf("OAuth2-%s-%s", OPT_AccessToken, KeyB64.Get()); + kAccTok = kAccTok.RStrip("="); + + if (Write) + { + Store->SetValue(kAccTok, v = AccessToken.Get()); + } + else + { + if (Store->GetValue(kAccTok, v)) + AccessToken = v.Str(); + else + return false; + } + + return true; + } +}; + +LOAuth2::LOAuth2(LOAuth2::Params ¶ms, const char *account, GDom *store, GStream *log) +{ + d = new LOAuth2Priv(params, account, store, log); + d->Serialize(false); +} + +LOAuth2::~LOAuth2() +{ + d->Serialize(true); + delete d; +} + +GString LOAuth2::GetAccessToken() +{ + if (d->AccessToken) + return d->AccessToken; + + if (d->GetToken()) + { + d->Log->Print("Got token.\n"); + if (d->GetAccess()) + { + return d->AccessToken; + } + } + else d->Log->Print("No token.\n"); + + return GString(); +} diff --git a/src/common/INet/Mail.cpp b/src/common/INet/Mail.cpp --- a/src/common/INet/Mail.cpp +++ b/src/common/INet/Mail.cpp @@ -1,4000 +1,4049 @@ /*hdr ** FILE: Mail.cpp ** AUTHOR: Matthew Allen ** DATE: 28/5/98 ** DESCRIPTION: Mail app ** ** Copyright (C) 1998, Matthew Allen ** fret@memecode.com */ #include #include #include #include #include "Lgi.h" #include "Mail.h" #include "GToken.h" #include "Base64.h" #include "INetTools.h" #include "LDateTime.h" #include "GDocView.h" #include "Store3Defs.h" #include "LgiRes.h" #include "../Hash/md5/md5.h" const char *sTextPlain = "text/plain"; const char *sTextHtml = "text/html"; const char *sTextXml = "text/xml"; const char *sApplicationInternetExplorer = "application/internet-explorer"; const char sMultipartMixed[] = "multipart/mixed"; const char sMultipartEncrypted[] = "multipart/encrypted"; const char sMultipartSigned[] = "multipart/signed"; const char sMultipartAlternative[] = "multipart/alternative"; const char sMultipartRelated[] = "multipart/related"; const char sAppOctetStream[] = "application/octet-stream"; ////////////////////////////////////////////////////////////////////////////////////////////////// LogEntry::LogEntry(GColour col) { c = col; } bool LogEntry::Add(const char *t, ssize_t len) { if (!t) return false; if (len < 0) len = strlen(t); /* // Strip off any whitespace on the end of the line. while (len > 0 && strchr(" \t\r\n", t[len-1])) len--; */ GAutoWString w(Utf8ToWide(t, len)); if (!w) return false; size_t ch = StrlenW(w); return Txt.Add(w, ch); } +bool Base64Str(GString &s) +{ + GString b64; + ssize_t Base64Len = BufferLen_BinTo64(s.Length()); + if (!b64.Set(NULL, Base64Len)) + return false; + + ssize_t Ch = ConvertBinaryToBase64(b64.Get(), b64.Length(), (uchar*)s.Get(), s.Length()); + LgiAssert(Ch == b64.Length()); + s = b64; + return true; +} + +bool UnBase64Str(GString &s) +{ + GString Bin; + ssize_t BinLen = BufferLen_64ToBin(s.Length()); + if (!Bin.Set(NULL, BinLen)) + return false; + + ssize_t Ch = ConvertBase64ToBinary((uchar*)Bin.Get(), Bin.Length(), s.Get(), s.Length()); + LgiAssert(Ch <= (int)Bin.Length()); + s = Bin; + s.Get()[Ch] = 0; + return true; +} + ////////////////////////////////////////////////////////////////////////////////////////////////// // return true if there are any characters with the 0x80 bit set bool Is8Bit(char *Text) { if (!Text) return false; while (*Text) { if (*Text & 0x80) return true; Text++; } return false; } // returns the maximum length of the lines contained in the string int MaxLineLen(char *Text) { if (!Text) return false; int Max = 0; int i = 0; for (char *c = Text; *c; c++) { if (*c == '\r') { // return } else if (*c == '\n') { // eol Max = MAX(i, Max); i = 0; } else { // normal char i++; } } return Max; } bool IsDotLined(char *Text) { if (Text) { for (char *l = Text; l && *l; ) { if (l[0] == '.') { if (l[1] == '\n' || l[1] == 0) { return true; } } l = strchr(l, '\n'); if (l) l++; } } return false; } char ConvHexToBin(char c) { if (c >= '0' && c <= '9') return c - '0'; if (c >= 'a' && c <= 'f') return c + 10 - 'a'; if (c >= 'A' && c <= 'F') return c + 10 - 'A'; return 0; } // Is s a valid non-whitespace string? bool ValidNonWSStr(const char *s) { if (s && *s) { while (*s && strchr(" \r\t\n", *s)) { s++; } if (*s) { return true; } } return false; } void TokeniseStrList(char *Str, List &Output, const char *Delim) { if (Str && Delim) { char *s = Str; while (*s) { while (*s && strchr(WhiteSpace, *s)) s++; char *e = s; for (; *e; e++) { if (strchr("\'\"", *e)) { // handle string constant char delim = *e++; e = strchr(e, delim); } else if (*e == '<') { e = strchr(e, '>'); } else { while (*e && *e != '<' && !IsWhiteSpace(*e) && !strchr(Delim, *e)) e++; } if (!e || !*e || strchr(Delim, *e)) { break; } } ssize_t Len = e ? e - s : strlen(s); if (Len > 0) { char *Temp = new char[Len+1]; if (Temp) { memcpy(Temp, s, Len); Temp[Len] = 0; Output.Insert(Temp); } } if (e) { s = e; for (; *s && strchr(Delim, *s); s++); } else break; } } } //////////////////////////////////////////////////////////////////////////////// char *DecodeBase64Str(char *Str, int Len) { if (Str) { ssize_t B64Len = (Len < 0) ? strlen(Str) : Len; ssize_t BinLen = BufferLen_64ToBin(B64Len); char *s = new char[BinLen+1]; if (s) { ssize_t Converted = ConvertBase64ToBinary((uchar*)s, BinLen, Str, B64Len); s[Converted] = 0; DeleteArray(Str); Str = s; } } return Str; } char *DecodeQuotedPrintableStr(char *Str, ssize_t Len) { if (Str) { if (Len < 0) Len = strlen(Str); uchar *s = new uchar[Len+1]; if (s) { char *Out = (char*) s; char *Text = Str; for (int i=0; i='a'&&(c)<='z')?(c)-'a'+'A':(c) ) #endif char *DecodeRfc2047(char *Str) { if (!Str) return NULL; GStringPipe p(256); for (char *s = Str; *s; ) { char *e = s; bool Decode = 0, Descape = 0; while (*e) { if ( (Decode = (e[0] == '=' && e[1] == '?')) || (Descape = (e[0] == '\\')) ) { // Emit characters between 's' and 'e' if (e > s) p.Write(s, e - s); break; } e++; } if (Decode) { // is there a word remaining bool Encoded = false; char *Start = e + 2; char *First = strchr(Start, '?'); char *Second = First ? strchr(First + 1, '?') : NULL; char *End = Second ? strstr(Second + 1, "?=") : NULL; if (End) { GString Cp(Start, First - Start); int Type = CONTENT_NONE; bool StripUnderscores = false; if (ToUpper(First[1]) == 'B') { // Base64 encoding Type = CONTENT_BASE64; } else if (ToUpper(First[1]) == 'Q') { // Quoted printable Type = CONTENT_QUOTED_PRINTABLE; StripUnderscores = true; } if (Type != CONTENT_NONE) { Second++; char *Block = NewStr(Second, End-Second); if (Block) { switch (Type) { case CONTENT_BASE64: Block = DecodeBase64Str(Block); break; case CONTENT_QUOTED_PRINTABLE: Block = DecodeQuotedPrintableStr(Block); break; } size_t Len = strlen(Block); if (StripUnderscores) { for (char *i=Block; *i; i++) { if (*i == '_') *i = ' '; } } if (Cp && !_stricmp(Cp, "utf-8")) { p.Write((uchar*)Block, Len); } else { GAutoString Utf8((char*)LgiNewConvertCp("utf-8", Block, Cp, Len)); if (Utf8) { if (LgiIsUtf8(Utf8)) p.Write((uchar*)Utf8.Get(), strlen(Utf8)); } else { p.Write((uchar*)Block, Len); } } DeleteArray(Block); } s = End + 2; if (*s == '\n') { s++; while (*s && strchr(WhiteSpace, *s)) s++; } Encoded = true; } } if (!Encoded) { // Encoding error, just emit the raw string and exit. size_t Len = strlen(s); p.Write((uchar*) s, Len); break; } } else if (Descape) { // Un-escape the string... e++; if (*e) p.Write(e, 1); else break; s = e + 1; } else { // Last segment of string... LgiAssert(*e == 0); if (e > s) p.Write(s, e - s); break; } } DeleteArray(Str); return p.NewStr(); } #define MIME_MAX_LINE 76 char *EncodeRfc2047(char *Str, const char *CodePage, List *CharsetPrefs, ssize_t LineLength) { if (!CodePage) { CodePage = "utf-8"; } GStringPipe p(256); if (!Str) return NULL; if (Is8Bit(Str)) { // pick an encoding bool Base64 = false; const char *DestCp = "utf-8"; size_t Len = strlen(Str);; if (_stricmp(CodePage, "utf-8") == 0) { DestCp = LgiDetectCharset(Str, Len, CharsetPrefs); } int Chars = 0; for (unsigned i=0; i 0 && ((double)Chars/Len) > 0.4 ) ) { Base64 = true; } char *Buf = (char*)LgiNewConvertCp(DestCp, Str, CodePage, Len); if (Buf) { // encode the word char Prefix[64]; int Ch = sprintf_s(Prefix, sizeof(Prefix), "=?%s?%c?", DestCp, Base64 ? 'B' : 'Q'); p.Write(Prefix, Ch); LineLength += Ch; if (Base64) { // Base64 size_t InLen = strlen(Buf); // int EstBytes = BufferLen_BinTo64(InLen); char Temp[512]; ssize_t Bytes = ConvertBinaryToBase64(Temp, sizeof(Temp), (uchar*)Buf, InLen); p.Push(Temp, Bytes); } else { // Quoted printable for (char *w = Buf; *w; w++) { if (*w == ' ') { if (LineLength > MIME_MAX_LINE - 3) { p.Print("?=\r\n\t%s", Prefix); LineLength = 1 + strlen(Prefix); } p.Write((char*)"_", 1); LineLength++; } else if (*w & 0x80 || *w == '_' || *w == '?' || *w == '=') { if (LineLength > MIME_MAX_LINE - 5) { p.Print("?=\r\n\t%s", Prefix); LineLength = 1 + strlen(Prefix); } char Temp[16]; Ch = sprintf_s(Temp, sizeof(Temp), "=%2.2X", (uchar)*w); p.Write(Temp, Ch); LineLength += Ch; } else { if (LineLength > MIME_MAX_LINE - 3) { p.Print("?=\r\n\t%s", Prefix); LineLength = 1 + strlen(Prefix); } p.Write(w, 1); LineLength++; } } } p.Push("?="); DeleteArray(Buf); } DeleteArray(Str); Str = p.NewStr(); } else { bool RecodeNewLines = false; for (char *s = Str; *s; s++) { if (*s == '\n' && (s == Str || s[-1] != '\r')) { RecodeNewLines = true; break; } } if (RecodeNewLines) { for (char *s = Str; *s; s++) { if (*s == '\r') ; else if (*s == '\n') p.Write("\r\n", 2); else p.Write(s, 1); } DeleteArray(Str); Str = p.NewStr(); } } return Str; } ////////////////////////////////////////////////////////////////////////////// void DeNullText(char *in, ssize_t &len) { char *out = in; char *end = in + len; while (in < end) { if (*in) { *out++ = *in; } else { len--; } in++; } } ////////////////////////////////////////////////////////////////////////////// typedef char CharPair[2]; static CharPair Pairs[] = { {'<', '>'}, {'(', ')'}, {'\'', '\''}, {'\"', '\"'}, {0, 0}, }; struct MailAddrPart { GAutoString Part; bool Brackets; bool ValidEmail; GAutoString RemovePairs(char *Str, ssize_t Len, CharPair *Pairs) { char *s = Str; if (Len < 0) Len = strlen(s); while (*s && strchr(WhiteSpace, *s)) { s++; Len--; } if (!*s) return GAutoString(); // Get the end of the string... char *e = s; if (Len < 0) e += strlen(s); else e += Len; // Seek back over any trailing whitespace while (e > s && strchr(WhiteSpace, e[-1])) e--; for (CharPair *p = Pairs; (*p)[0]; p++) { if ((*p)[0] == *s && (*p)[1] == e[-1]) { s++; e--; if (s < e) { // reset search p = Pairs - 1; } else break; } } Len = e - s; if (Len < 0) return GAutoString(); return GAutoString(NewStr(s, Len)); } MailAddrPart(char *s, ssize_t len) { ValidEmail = false; Brackets = false; if (s) { if (len < 0) len = strlen(s); while (strchr(WhiteSpace, *s) && len > 0) { s++; len--; } Brackets = *s == '<'; Part = RemovePairs(s, len, Pairs); // ValidEmail = IsValidEmail(Part); } } int Score() { if (!Part) return 0; return (ValidEmail ? 1 : 0) + (Brackets ? 1 : 0); } }; int PartCmp(GAutoPtr *a, GAutoPtr *b) { return (*b)->Score() - (*a)->Score(); } void DecodeAddrName(const char *Str, GAutoString &Name, GAutoString &Addr, const char *DefaultDomain) { /* Testing code char *Input[] = { "\"Sound&Secure@speedytechnical.com\" ", "\"@MM-Social Mailman List\" ", "'Matthew Allen (fret)' ", "Matthew Allen (fret) ", "\"'Matthew Allen'\" ", "Matthew Allen", "fret@memecode.com", "\"\" ", " (fret@memecode.com)", "Matthew Allen ", "\"Matthew, Allen\" (fret@memecode.com)", "Matt'hew Allen ", "john.omalley ", "Bankers' Association (ABA)", "'Amy's Mum' ", "\"Philip Doggett (JIRA)\" ", 0 }; GAutoString Name, Addr; for (char **i = Input; *i; i++) { Name.Reset(); Addr.Reset(); DecodeAddrName(*i, Name, Addr, "name.com"); LgiTrace("N=%-#32s A=%-32s\n", Name, Addr); } */ if (!Str) return; GArray< GAutoPtr > Parts; GString s = Str; GString non; GString email; GString::Array a = s.SplitDelimit("<>"); for (unsigned i=0; i 0) { const char *ChSet = " \t\r\n\'\"<>"; do { non = non.Strip(ChSet); } while (non.Length() > 0 && strchr(ChSet, non(0))); } Name.Reset(NewStr(non)); Addr.Reset(NewStr(email.Strip())); } void StrCopyToEOL(char *d, char *s) { if (d && s) { while (*s && *s != '\r' && *s != '\n') { *d++ = *s++; } *d = 0; } } ////////////////////////////////////////////////////////////////////////////////////////////////// MailTransaction::MailTransaction() { Index = -1; Flags = 0; Status = false; Oversize = false; Stream = 0; UserData = 0; } MailTransaction::~MailTransaction() { } ////////////////////////////////////////////////////////////////////////////////////////////////// FileDescriptor::FileDescriptor() { Embeded = 0; Offset = 0; Size = 0; Data = 0; MimeType = 0; ContentId = 0; Lock = 0; OwnEmbeded = false; } FileDescriptor::FileDescriptor(GStreamI *embed, int64 offset, int64 size, char *name) { Embeded = embed; Offset = offset; Size = size; Data = 0; MimeType = 0; Lock = 0; ContentId = 0; OwnEmbeded = false; if (name) { Name(name); } } FileDescriptor::FileDescriptor(char *name) { Embeded = 0; Offset = 0; Size = 0; Data = 0; MimeType = 0; Lock = 0; ContentId = 0; OwnEmbeded = false; if (name) { Name(name); if (File.Open(name, O_READ)) { Size = File.GetSize(); File.Close(); } } } FileDescriptor::FileDescriptor(char *data, int64 len) { Embeded = 0; Offset = 0; MimeType = 0; Lock = 0; ContentId = 0; Size = len; OwnEmbeded = false; Data = data ? new uchar[(size_t)Size] : 0; if (Data) { memcpy(Data, data, (size_t)Size); } } FileDescriptor::~FileDescriptor() { if (OwnEmbeded) { DeleteObj(Embeded); } DeleteArray(MimeType); DeleteArray(ContentId); DeleteArray(Data); } void FileDescriptor::SetOwnEmbeded(bool i) { OwnEmbeded = i; } void FileDescriptor::SetLock(LMutex *l) { Lock = l; } LMutex *FileDescriptor::GetLock() { return Lock; } GStreamI *FileDescriptor::GotoObject() { if (Embeded) { Embeded->SetPos(Offset); return Embeded; } else if (Name() && File.Open(Name(), O_READ)) { return &File; } else if (Data && Size > 0) { DataStream.Reset(new GMemStream(Data, Size, false)); return DataStream; } return 0; } int FileDescriptor::Sizeof() { return (int)Size; } uchar *FileDescriptor::GetData() { return Data; } bool FileDescriptor::Decode(char *ContentType, char *ContentTransferEncoding, char *MimeData, int MimeDataLen) { bool Status = false; int Content = CONTENT_NONE; if (ContentType && ContentTransferEncoding) { // Content-Type: application/octet-stream; name="Scribe.opt" Content = CONTENT_OCTET_STREAM; if (strnistr(ContentTransferEncoding, "base64", 1000)) { Content = CONTENT_BASE64; } if (strnistr(ContentTransferEncoding, "quoted-printable", 1000)) { Content = CONTENT_QUOTED_PRINTABLE; } if (Content != CONTENT_NONE) { const char *NameKey = "name"; char *n = strnistr(ContentType, NameKey, 1000); if (n) { char *Equal = strchr(n, '='); if (Equal) { Equal++; while (*Equal && *Equal == '\"') { Equal++; } char *End = strchr(Equal, '\"'); if (End) { *End = 0; } Name(Equal); Status = true; } } } } if (Status && MimeData && MimeDataLen > 0 && Content != CONTENT_NONE) { Status = false; char *Base64 = new char[MimeDataLen]; switch (Content) { case CONTENT_OCTET_STREAM: { Size = 0; DeleteObj(Data); Data = new uchar[MimeDataLen]; if (Data) { Size = MimeDataLen; memcpy(Data, MimeData, (size_t)Size); Status = true; } break; } case CONTENT_QUOTED_PRINTABLE: { Size = 0; DeleteObj(Data); Data = new uchar[MimeDataLen+1]; if (Data) { char *Out = (char*) Data; for (int i=0; i= Size - 3; if (Status) { Size = Converted; } else { DeleteArray(Data); Size = 0; } } break; } } } return Status; } ////////////////////////////////////////////////////////////////////////////////////////////////// AddressDescriptor::AddressDescriptor(AddressDescriptor *Copy) { Data = 0; Status = Copy ? Copy->Status : false; CC = Copy ? Copy->CC : false; Addr = Copy ? NewStr(Copy->Addr) : 0; Name = Copy ? NewStr(Copy->Name) : 0; } AddressDescriptor::~AddressDescriptor() { _Delete(); } void AddressDescriptor::_Delete() { Data = 0; Status = false; CC = 0; DeleteArray(Name); DeleteArray(Addr); } void AddressDescriptor::Print(char *Str, int Len) { if (!Str) { LgiAssert(0); return; } if (Addr && Name) { sprintf_s(Str, Len, "%s (%s)", Addr, Name); } else if (Addr) { strcpy_s(Str, Len, Addr); } else if (Name) { sprintf_s(Str, Len, "(%s)", Name); } } ////////////////////////////////////////////////////////////////////////////////////////////////// MailProtocol::MailProtocol() { Buffer[0] = 0; Logger = 0; ErrMsgId = 0; + SettingStore = NULL; Items = 0; Transfer = 0; } MailProtocol::~MailProtocol() { CharsetPrefs.DeleteArrays(); } void MailProtocol::Log(const char *Str, GSocketI::SocketMsgType type) { if (Logger && Str) { char s[1024]; char *e = s + sizeof(s) - 2; const char *i = Str; char *o = s; while (*i && o < e) { *o++ = *i++; } while (o > s && (o[-1] == '\r' || o[-1] == '\n')) o--; *o++ = '\n'; *o = 0; Logger->Write(s, o - s, type); } } bool MailProtocol::Error(const char *file, int line, const char *msg, ...) { char s[1024]; va_list a; va_start(a, msg); vsprintf_s(s, sizeof(s), msg, a); va_end(a); Log(s, GSocketI::SocketMsgError); LgiTrace("%s:%i - Error: %s", file, line, s); return false; } bool MailProtocol::Read() { bool Status = false; if (Socket) { Status = Socket->Read(Buffer, sizeof(Buffer), 0) > 0; } return Status; } bool MailProtocol::Write(const char *Buf, bool LogWrite) { bool Status = false; if (Socket) { const char *p = Buf ? Buf : Buffer; Status = Socket->Write(p, strlen(p), 0) > 0; if (LogWrite) { Log(p, GSocketI::SocketMsgSend); } } return Status; } ////////////////////////////////////////////////////////////////////////////////////////////////// #define VERIFY_RET_VAL(Arg) \ { \ if (!Arg) \ { \ LMutex::Auto Lck(&SocketLock, _FL); \ Socket.Reset(0); \ return NULL; \ } \ } #define VERIFY_ONERR(Arg) \ { \ if (!Arg) \ { \ LMutex::Auto Lck(&SocketLock, _FL); \ Socket.Reset(0); \ goto CleanUp; \ } \ } void Reorder(GArray &a, const char *s) { for (unsigned i=0; i 0) { a.DeleteAt(i, true); a.AddAt(0, s); break; } } } MailSmtp::MailSmtp() { } MailSmtp::~MailSmtp() { } bool MailSmtp::Open(GSocketI *S, const char *RemoteHost, const char *LocalDomain, const char *UserName, const char *Password, int Port, int Flags) { char Str[256] = ""; bool Status = false; if (!RemoteHost) Error(_FL, "No remote SMTP host.\n"); else { strcpy_s(Str, sizeof(Str), RemoteHost); char *Colon = strchr(Str, ':'); if (Colon) { *Colon = 0; Colon++; Port = atoi(Colon); } if (Port == 0) { if (Flags & MAIL_SSL) Port = SMTP_SSL_PORT; else Port = SMTP_PORT; } GAutoString Server(TrimStr(Str)); if (Server) { if (SocketLock.Lock(_FL)) { Socket.Reset(S); SocketLock.Unlock(); } Socket->SetTimeout(30 * 1000); char Msg[256]; sprintf_s(Msg, sizeof(Msg), "Connecting to %s:%i...", Server.Get(), Port); Log(Msg, GSocketI::SocketMsgInfo); if (!Socket->Open(Server, Port)) Error(_FL, "Failed to connect socket to %s:%i\n", Server.Get(), Port); else { GStringPipe Str; // receive signon message VERIFY_RET_VAL(ReadReply("220")); // Rfc 2554 ESMTP authentication SmtpHello: sprintf_s(Buffer, sizeof(Buffer), "EHLO %s\r\n", (ValidNonWSStr(LocalDomain)) ? LocalDomain : "default"); VERIFY_RET_VAL(Write(0, true)); /*bool HasSmtpExtensions =*/ ReadReply("250", &Str); bool Authed = false; bool NoAuthTypes = false; bool SupportsStartTLS = false; GArray AuthTypes; // Look through the response for the auth line char *Response = Str.NewStr(); if (Response) { GToken Lines(Response, "\n"); for (uint32_t i=0; iSetValue(GSocket_Protocol, v="SSL")) { Flags &= ~MAIL_USE_STARTTLS; goto SmtpHello; } else { // SSL init failed... what to do here? return false; } } if (ValidStr(UserName) && ValidStr(Password)) { if (AuthTypes.Length() == 0) { // No auth types? huh? if (TestFlag(Flags, MAIL_USE_AUTH)) { if (TestFlag(Flags, MAIL_USE_PLAIN)) // Force plain type AuthTypes.Add("PLAIN"); else if (TestFlag(Flags, MAIL_USE_LOGIN)) // Force login type AuthTypes.Add("LOGIN"); else if (TestFlag(Flags, MAIL_USE_CRAM_MD5)) // Force CRAM MD5 type AuthTypes.Add("CRAM-MD5"); + else if (TestFlag(Flags, MAIL_USE_OAUTH2)) + // Force OAUTH2 type + AuthTypes.Add("XOAUTH2"); else { // Try all AuthTypes.Add("PLAIN"); AuthTypes.Add("LOGIN"); AuthTypes.Add("CRAM-MD5"); + AuthTypes.Add("XOAUTH2"); } } else { NoAuthTypes = true; } } else { if (TestFlag(Flags, MAIL_USE_AUTH)) { // Force user preference if (TestFlag(Flags, MAIL_USE_PLAIN)) Reorder(AuthTypes, "PLAIN"); else if (TestFlag(Flags, MAIL_USE_LOGIN)) Reorder(AuthTypes, "LOGIN"); else if (TestFlag(Flags, MAIL_USE_CRAM_MD5)) Reorder(AuthTypes, "CRAM-MD5"); + else if (TestFlag(Flags, MAIL_USE_OAUTH2)) + Reorder(AuthTypes, "XOAUTH2"); } } for (auto Auth : AuthTypes) { // Try all their auth types against our internally support types if (Auth.Equals("LOGIN")) { VERIFY_RET_VAL(Write("AUTH LOGIN\r\n", true)); VERIFY_RET_VAL(ReadReply("334")); ZeroObj(Buffer); ConvertBinaryToBase64(Buffer, sizeof(Buffer), (uchar*)UserName, strlen(UserName)); strcat(Buffer, "\r\n"); VERIFY_RET_VAL(Write(0, true)); if (ReadReply("334")) { ZeroObj(Buffer); ConvertBinaryToBase64(Buffer, sizeof(Buffer), (uchar*)Password, strlen(Password)); strcat(Buffer, "\r\n"); VERIFY_RET_VAL(Write(0, true)); if (ReadReply("235")) { Authed = true; } } } else if (Auth.Equals("PLAIN")) { char Tmp[256]; ZeroObj(Tmp); int ch = 1; ch += sprintf_s(Tmp+ch, sizeof(Tmp)-ch, "%s", UserName) + 1; ch += sprintf_s(Tmp+ch, sizeof(Tmp)-ch, "%s", Password) + 1; char B64[256]; ZeroObj(B64); ConvertBinaryToBase64(B64, sizeof(B64), (uint8_t*)Tmp, ch); sprintf_s(Buffer, sizeof(Buffer), "AUTH PLAIN %s\r\n", B64); VERIFY_RET_VAL(Write(0, true)); if (ReadReply("235")) { Authed = true; } } else if (Auth.Equals("CRAM-MD5")) { sprintf_s(Buffer, sizeof(Buffer), "AUTH CRAM-MD5\r\n"); VERIFY_RET_VAL(Write(0, true)); if (ReadReply("334")) { auto Sp = strchr(Buffer, ' '); if (Sp) { Sp++; // Decode the server response: uint8_t Txt[128]; auto InLen = strlen(Sp); ssize_t TxtLen = ConvertBase64ToBinary(Txt, sizeof(Txt), Sp, InLen); // Calc the hash: // https://tools.ietf.org/html/rfc2104 char Key[64] = {0}; memcpy(Key, Password, MIN(strlen(Password), sizeof(Key))); uint8_t iKey[256]; char oKey[256]; for (unsigned i=0; i<64; i++) { iKey[i] = Key[i] ^ 0x36; oKey[i] = Key[i] ^ 0x5c; } memcpy(iKey+64, Txt, TxtLen); md5_state_t md5; md5_init(&md5); md5_append(&md5, iKey, 64 + TxtLen); md5_finish(&md5, oKey + 64); md5_init(&md5); md5_append(&md5, (uint8_t*)oKey, 64 + 16); char digest[16]; md5_finish(&md5, digest); char r[256]; int ch = sprintf_s(r, sizeof(r), "%s ", UserName); for (unsigned i=0; i<16; i++) ch += sprintf_s(r+ch, sizeof(r)-ch, "%02x", (uint8_t)digest[i]); // Base64 encode ssize_t Len = ConvertBinaryToBase64(Buffer, sizeof(Buffer), (uint8_t*)r, ch); Buffer[Len++] = '\r'; Buffer[Len++] = '\n'; Buffer[Len++] = 0; VERIFY_RET_VAL(Write(0, true)); if (ReadReply("235")) Authed = true; } } } + else if (Auth.Equals("XOAUTH2")) + { + LOAuth2 OAuth2(OAuth2, UserName, SettingStore); + auto Tok = OAuth2.GetAccessToken(); + if (Tok) + { + GString s; + s.Printf("user=%s\001auth=Bearer %s\001\001\0", UserName, Tok.Get()); + Base64Str(s); + + sprintf_s(Buffer, sizeof(Buffer), "AUTH %s %s\r\n", Auth.Get(), s.Get()); + VERIFY_RET_VAL(Write(0, true)); + Authed = ReadReply("235"); + } + } else { LgiTrace("%s:%i - Unsupported auth type '%s'\n", _FL, Auth.Get()); } if (Authed) break; } if (!Authed) { if (NoAuthTypes) SetError(L_ERROR_ESMTP_NO_AUTHS, "The server didn't return the authentication methods it supports."); else { GString p; for (auto i : AuthTypes) { if (p.Get()) p += ", "; p += i; } SetError(L_ERROR_UNSUPPORTED_AUTH, "Authentication failed, types available:\n\t%s", p); } } Status = Authed; } else { Status = true; } } } } return Status; } bool MailSmtp::WriteText(const char *Str) { // we have to convert all strings to CRLF in here bool Status = false; if (Str) { GMemQueue Temp; const char *Start = Str; while (*Str) { if (*Str == '\n') { // send a string ssize_t Size = Str-Start; if (Str[-1] == '\r') Size--; Temp.Write((uchar*) Start, Size); Temp.Write((uchar*) "\r\n", 2); Start = Str + 1; } Str++; } // send the final string ssize_t Size = Str-Start; if (Str[-1] == '\r') Size--; Temp.Write((uchar*) Start, (int)Size); Size = (int)Temp.GetSize(); char *Data = new char[(size_t)Size]; if (Data) { Temp.Read((uchar*) Data, Size); Status = Socket->Write(Data, (int)Size, 0) == Size; DeleteArray(Data); } } return Status; } char *StripChars(char *Str, const char *Chars = "\r\n") { if (Str) { char *i = Str; char *o = Str; while (*i) { if (strchr(Chars, *i)) i++; else *o++ = *i++; } *o++ = 0; } return Str; } char *CreateAddressTag(List &l, int Type, List *CharsetPrefs) { char *Result = 0; List Addr; AddressDescriptor *a; for (a = l.First(); a; a = l.Next()) { if (a->CC == Type) { Addr.Insert(a); } } if (Addr.Length() > 0) { GStringPipe StrBuf; StrBuf.Push((Type == 0) ? (char*)"To: " : (char*)"Cc: "); for (a = Addr.First(); a; ) { AddressDescriptor *NextA = Addr.Next(); char Buffer[256] = ""; StripChars(a->Name); StripChars(a->Addr); if (a->Addr && strchr(a->Addr, ',')) { // Multiple address format GToken t(a->Addr, ","); for (uint32_t i=0; i", t[i]); if (i < t.Length()-1) strcat(Buffer, ",\r\n\t"); StrBuf.Push(Buffer); Buffer[0] = 0; } } else if (a->Name) { // Name and addr char *Mem = 0; char *Name = a->Name; if (Is8Bit(Name)) { Name = Mem = EncodeRfc2047(NewStr(Name), 0, CharsetPrefs); } if (strchr(Name, '\"')) sprintf_s(Buffer, sizeof(Buffer), "'%s' <%s>", Name, a->Addr); else sprintf_s(Buffer, sizeof(Buffer), "\"%s\" <%s>", Name, a->Addr); DeleteArray(Mem); } else if (a->Addr) { // Just addr sprintf_s(Buffer, sizeof(Buffer), "<%s>", a->Addr); } if (NextA) strcat(Buffer, ",\r\n\t"); StrBuf.Push(Buffer); a = NextA; } StrBuf.Push("\r\n"); Result = StrBuf.NewStr(); } return Result; } // This class implements a pipe that writes to a socket class SocketPipe : public GStringPipe { GSocketI *s; MailProtocolProgress *p; public: bool Status; SocketPipe(GSocketI *socket, MailProtocolProgress *progress) { s = socket; p = progress; Status = true; } ssize_t Read(void *Ptr, ssize_t Size, int Flags) { return false; } int64 SetSize(int64 Size) { if (p) { p->Start = LgiCurrentTime(); p->Range = (int)Size; return Size; } return -1; } ssize_t Write(const void *InPtr, ssize_t Size, int Flags) { char *Ptr = (char*)InPtr; char *e = Ptr + Size; while (Ptr < e) { ssize_t w = s->Write(Ptr, e - Ptr, 0); if (w > 0) { Ptr += w; if (p && p->Range && w > 0) p->Value += w; } else break; } return Ptr - (char*)InPtr; } }; bool MailSmtp::SendToFrom(List &To, AddressDescriptor *From, MailProtocolError *Err) { bool AddrOk = false; if (To.Length() == 0) { ErrMsgId = L_ERROR_ESMTP_NO_RECIPIENT; ErrMsgFmt = "No recipients to send to."; ErrMsgParam.Empty(); return false; } // send MAIL message if (From && ValidStr(From->Addr)) { sprintf_s(Buffer, sizeof(Buffer), "MAIL FROM: <%s>\r\n", From->Addr); } else { ErrMsgId = L_ERROR_ESMTP_NO_FROM; ErrMsgFmt = "No 'from' address in email."; ErrMsgParam.Empty(); return false; } VERIFY_RET_VAL(Write(0, true)); VERIFY_RET_VAL(ReadReply("250", 0, Err)); // send RCPT message AddrOk = true; List::I Recip = To.begin(); for (AddressDescriptor *a = *Recip; a; a = *++Recip) { char *Addr = ValidStr(a->Addr) ? a->Addr : a->Name; if (ValidStr(Addr)) { GToken Parts(Addr, ","); for (unsigned p=0; p\r\n", Parts[p]); VERIFY_RET_VAL(Write(0, true)); a->Status = ReadReply("25", 0, Err); AddrOk |= a->Status != 0; // at least one address is ok } } else if (Err) { ErrMsgId = L_ERROR_ESMTP_BAD_RECIPIENT; ErrMsgFmt = "Invalid recipient '%s'."; ErrMsgParam = Addr; } } return AddrOk; } GStringPipe *MailSmtp::SendData(MailProtocolError *Err) { // send DATA message sprintf_s(Buffer, sizeof(Buffer), "DATA\r\n"); VERIFY_RET_VAL(Write(0, true)); VERIFY_RET_VAL(ReadReply("354", 0, Err)); return new SocketPipe(Socket, Transfer); } GStringPipe *MailSmtp::SendStart(List &To, AddressDescriptor *From, MailProtocolError *Err) { return SendToFrom(To, From, Err) ? SendData(Err) : NULL; } bool MailSmtp::SendEnd(GStringPipe *m) { bool Status = false; SocketPipe *Msg = dynamic_cast(m); if (Msg) { // send message terminator and receive reply if (Msg->Status && Msg->Write((void*)"\r\n.\r\n", 5, 0)) { Status = ReadReply("250"); } // else // just close the connection on them // so nothing gets sent } DeleteObj(m); return Status; } /* bool MailSmtp::Send(MailMessage *Msg, bool Mime) { bool Status = false; if (Socket && Msg) { GStringPipe *Sink = SendStart(Msg->To, Msg->From); if (Sink) { // setup a gui progress meter to send the email, // the length is just a guesstimate as we won't know the exact // size until we encode it all, and I don't want it hanging around // in memory at once, so we encode and send on the fly. int Length = 1024 + (Msg->GetBody() ? strlen(Msg->GetBody()) : 0); for (FileDescriptor *f=Msg->FileDesc.First(); f; f=Msg->FileDesc.Next()) { Length += f->Sizeof() * 4 / 3; } // encode and send message for transport Msg->Encode(*Sink, 0, this); Status = SendEnd(Sink); } } return Status; } */ bool MailSmtp::Close() { if (Socket) { // send QUIT message sprintf_s(Buffer, sizeof(Buffer), "QUIT\r\n"); VERIFY_RET_VAL(Write(0, true)); VERIFY_RET_VAL(ReadReply("221")); LMutex::Auto Lock(&SocketLock, _FL); Socket.Reset(0); return true; } return false; } bool MailSmtp::ReadReply(const char *Str, GStringPipe *Pipe, MailProtocolError *Err) { bool Status = false; if (Socket && Str) { int Pos = 0; char *Start = Buffer; ZeroObj(Buffer); while (Pos < sizeof(Buffer)) { ssize_t Len = Socket->Read(Buffer+Pos, sizeof(Buffer)-Pos, 0); if (Len > 0) { char *Eol = strstr(Start, "\r\n"); while (Eol) { // wipe EOL chars *Eol++ = 0; *Eol++ = 0; // process if (Pipe) { if (Pipe->GetSize()) Pipe->Push("\n"); Pipe->Push(Start); } if (Start[3] == ' ') { // end of response if (!strncmp(Start, Str, strlen(Str))) { Status = true; } if (Err) { Err->Code = atoi(Start); char *Sp = strchr(Start, ' '); Err->ErrMsg = Sp ? Sp + 1 : Start; } // Log Log(Start, atoi(Start) >= 400 ? GSocketI::SocketMsgError : GSocketI::SocketMsgReceive); // exit loop Pos = sizeof(Buffer); break; } else { Log(Start, GSocketI::SocketMsgReceive); // more lines follow Start = Eol; Eol = strstr(Start, "\r\n"); } } Pos += Len; } else break; } if (!Status) { SetError(L_ERROR_GENERIC, "Error: %s", Buffer); } } return Status; } ////////////////////////////////////////////////////////////////////////////////////////////////// class Mail2Folder : public GStringPipe { char File[256]; GFile F; public: Mail2Folder(char *Path, List &To) { do { char n[32]; sprintf_s(n, sizeof(n), "%u.mail", LgiRand()); LgiMakePath(File, sizeof(File), Path, n); } while (FileExists(File)); if (F.Open(File, O_WRITE)) { F.Print("Forward-Path: "); int i = 0; for (AddressDescriptor *a=To.First(); a; a=To.Next()) { a->Status = true; GToken Addrs(a->Addr, ","); for (unsigned n=0; n", Addrs[n]); } } F.Print("\r\n"); } } ~Mail2Folder() { F.Close(); } ssize_t Read(void *Buffer, ssize_t Size, int Flags = 0) { return F.Read(Buffer, Size, Flags); } ssize_t Write(const void *Buffer, ssize_t Size, int Flags = 0) { return F.Write(Buffer, Size, Flags); } }; class MailPostFolderPrivate { public: char *Path; MailPostFolderPrivate() { Path = 0; } ~MailPostFolderPrivate() { DeleteArray(Path); } }; MailSendFolder::MailSendFolder(char *Path) { d = new MailPostFolderPrivate; d->Path = NewStr(Path); } MailSendFolder::~MailSendFolder() { DeleteObj(d); } bool MailSendFolder::Open(GSocketI *S, const char *RemoteHost, const char *LocalDomain, const char *UserName, const char *Password, int Port, int Flags) { return DirExists(d->Path); } bool MailSendFolder::Close() { return true; } GStringPipe *MailSendFolder::SendStart(List &To, AddressDescriptor *From, MailProtocolError *Err) { return new Mail2Folder(d->Path, To); } bool MailSendFolder::SendEnd(GStringPipe *Sink) { DeleteObj(Sink); return true; } ////////////////////////////////////////////////////////////////////////////////////////////////// class MailItem { public: char *File; bool Delete; MailItem(char *f) { File = NewStr(f); Delete = false; } ~MailItem() { DeleteArray(File); } }; class MailReceiveFolderPrivate { public: char *Path; List Mail; MailReceiveFolderPrivate() { Path = 0; } ~MailReceiveFolderPrivate() { DeleteArray(Path); Mail.DeleteObjects(); } void Empty() { for (MailItem *m = Mail.First(); m; m = Mail.Next()) { if (m->Delete) { FileDev->Delete(m->File, false); } } Mail.DeleteObjects(); } }; MailReceiveFolder::MailReceiveFolder(char *Path) { d = new MailReceiveFolderPrivate; d->Path = NewStr(Path); } MailReceiveFolder::~MailReceiveFolder() { DeleteObj(d); } bool MailReceiveFolder::Open(GSocketI *S, const char *RemoteHost, int Port, const char *User, const char *Password, GDom *SettingStore, int Flags) { // We don't use the socket so just free it here... DeleteObj(S); // Argument check if (!DirExists(d->Path)) return false; GDirectory Dir; // Loop through files, looking for email for (int b = Dir.First(d->Path, LGI_ALL_FILES); b; b = Dir.Next()) { if (!Dir.IsDir()) { if (MatchStr("*.eml", Dir.GetName()) || MatchStr("*.mail", Dir.GetName())) { char p[300]; Dir.Path(p, sizeof(p)); d->Mail.Insert(new MailItem(p)); } } } return true; } bool MailReceiveFolder::Close() { d->Empty(); return true; } int MailReceiveFolder::GetMessages() { return (int)d->Mail.Length(); } bool MailReceiveFolder::Receive(GArray &Trans, MailCallbacks *Callbacks) { bool Status = false; for (unsigned i=0; iStream) { t->Status = false; MailItem *m = d->Mail[t->Index]; if (m) { GFile i; if (i.Open(m->File, O_READ)) { GCopyStreamer c; if (c.Copy(&i, t->Stream)) { Status = t->Status = true; if (Callbacks && Callbacks->OnReceive) { Callbacks->OnReceive(t, Callbacks->CallbackData); } } } } } } return Status; } bool MailReceiveFolder::Delete(int Message) { MailItem *m = d->Mail[Message]; if (m) { m->Delete = true; return false; } return false; } int MailReceiveFolder::Sizeof(int Message) { MailItem *m = d->Mail[Message]; if (m) { return (int)LgiFileSize(m->File); } return 0; } bool MailReceiveFolder::GetUid(int Message, char *Id, int IdLen) { if (Id) { MailItem *m = d->Mail[Message]; if (m) { char *s = strrchr(m->File, DIR_CHAR); if (s++) { char *e = strchr(s, '.'); if (!e) e = s + strlen(s); ssize_t Len = e - s; memcpy(Id, s, Len); Id[Len] = 0; return true; } } } return false; } bool MailReceiveFolder::GetUidList(List &Id) { bool Status = false; for (int i=0; iMail.Length(); i++) { char Uid[256]; if (GetUid(i, Uid, sizeof(Uid))) { Status = true; Id.Insert(NewStr(Uid)); } else { Id.DeleteArrays(); Status = false; break; } } return Status; } char *MailReceiveFolder::GetHeaders(int Message) { MailItem *m = d->Mail[Message]; if (m) { GFile i; if (i.Open(m->File, O_READ)) { GStringPipe o; GCopyStreamer c; GLinePrefix e("", false); if (c.Copy(&i, &o, &e)) { return o.NewStr(); } } } return 0; } ////////////////////////////////////////////////////////////////////////////////////////////////// MailPop3::MailPop3() { End = "\r\n.\r\n"; Marker = End; Messages = -1; } MailPop3::~MailPop3() { } int MailPop3::GetMessages() { if (Messages < 0) { if (Socket && Socket->IsOpen()) { // see how many messages there are VERIFY_ONERR(Write("STAT\r\n", true)); VERIFY_ONERR(ReadReply()); Messages = GetInt(); } else LgiAssert(!"No socket to get message count."); } CleanUp: return Messages; } int MailPop3::GetInt() { char Buf[32]; char *Start = strchr(Buffer, ' '); if (Start) { Start++; char *End = strchr(Start, ' '); if (End) { int Len = (int) (End - Start); memcpy(Buf, Start, Len); Buf[Len] = 0; return atoi(Buf); } } return 0; } bool MailPop3::ReadReply() { bool Status = false; if (Socket) { int Pos = 0; ZeroObj(Buffer); do { ssize_t Result = Socket->Read(Buffer+Pos, sizeof(Buffer)-Pos, 0); if (Result <= 0) // an error? { // Leave the loop... break; } Pos += Result; } while ( !strstr(Buffer, "\r\n") && sizeof(Buffer)-Pos > 0); Status = (Buffer[0] == '+') && strstr(Buffer, "\r\n"); char *Cr = strchr(Buffer, '\r'); if (Cr) *Cr = 0; if (ValidStr(Buffer)) Log(Buffer, (Status) ? GSocketI::SocketMsgReceive : GSocketI::SocketMsgError); if (Cr) *Cr = '\r'; if (!Status) { SetError(L_ERROR_GENERIC, "Error: %s", Buffer); } } return Status; } bool MailPop3::ListCmd(const char *Cmd, LHashTbl, bool> &Results) { sprintf_s(Buffer, sizeof(Buffer), "%s\r\n", Cmd); if (!Write(0, true)) return false; char *b = Buffer; ssize_t r; while ((r = Socket->Read(b, sizeof(Buffer)-(b-Buffer))) > 0) { b += r; if (strnstr(Buffer, "\r\n.\r\n", b-Buffer)) break; } if (r <= 0) return false; GToken t(Buffer, "\r\n"); for (unsigned i=1; iGetValue("IsSSL", IsSsl) && IsSsl.CastInt32()) Port = POP3_SSL_PORT; else Port = POP3_PORT; } strcpy_s(Str, sizeof(Str), RemoteHost); char *Colon = strchr(Str, ':'); if (Colon) { *Colon = 0; Colon++; Port = atoi(Colon); } if (S && User && Password && (Server = TrimStr(Str))) { S->SetTimeout(30 * 1000); ReStartConnection: if (SocketLock.Lock(_FL)) { Socket.Reset(S); SocketLock.Unlock(); } if (Socket && Socket->Open(Server, Port) && ReadReply()) { GVariant NoAPOP = false; if (SettingStore) SettingStore->GetValue(OPT_Pop3NoApop, NoAPOP); if (!NoAPOP.CastInt32()) { char *s = strchr(Buffer + 3, '<'); if (s) { char *e = strchr(s + 1, '>'); if (e) { Apop = NewStr(s, e - s + 1); } } } // login bool Authed = false; char *user = (char*) LgiNewConvertCp("iso-8859-1", User, "utf-8"); char *pass = (char*) LgiNewConvertCp("iso-8859-1", Password, "utf-8"); if (user && (pass || SecureAuth)) { bool SecurityError = false; if (TestFlag(Flags, MAIL_USE_STARTTLS)) { strcpy_s(Buffer, sizeof(Buffer), "STARTTLS\r\n"); VERIFY_RET_VAL(Write(0, true)); VERIFY_RET_VAL(ReadReply()); GVariant v; if (Socket->SetValue(GSocket_Protocol, v="SSL")) { Flags &= ~MAIL_USE_STARTTLS; } else { SecurityError = true; } } if (!SecurityError && Apop) // GotKey, not implemented { // using encrypted password unsigned char Digest[16]; char HexDigest[33]; // append password char Key[256]; sprintf_s(Key, sizeof(Key), "%s%s", Apop, pass); ZeroObj(Digest); MDStringToDigest(Digest, Key); for (int i = 0; i < 16; i++) sprintf_s(HexDigest + (i*2), 3, "%2.2x", Digest[i]); HexDigest[32] = 0; sprintf_s(Buffer, sizeof(Buffer), "APOP %s %s\r\n", user, HexDigest); VERIFY_ONERR(Write(0, true)); Authed = ReadReply(); if (!Authed) { DeleteArray(Apop); GVariant NoAPOP = true; if (SettingStore) SettingStore->SetValue(OPT_Pop3NoApop, NoAPOP); S->Close(); goto ReStartConnection; } } if (!SecurityError && SecureAuth) { LHashTbl, bool> AuthTypes, Capabilities; if (ListCmd("AUTH", AuthTypes) && ListCmd("CAPA", Capabilities)) { if (AuthTypes.Find("GSSAPI")) { sprintf_s(Buffer, sizeof(Buffer), "AUTH GSSAPI\r\n"); VERIFY_ONERR(Write(0, true)); VERIFY_ONERR(ReadReply()); // http://www.faqs.org/rfcs/rfc2743.html } } } else if (!SecurityError && !Authed) { // have to use non-key method sprintf_s(Buffer, sizeof(Buffer), "USER %s\r\n", user); VERIFY_ONERR(Write(0, true)); VERIFY_ONERR(ReadReply()); sprintf_s(Buffer, sizeof(Buffer), "PASS %s\r\n", pass); VERIFY_ONERR(Write(0, false)); Log("PASS *******", GSocketI::SocketMsgSend); Authed = ReadReply(); } DeleteArray(user); DeleteArray(pass); } if (Authed) { Status = true; } else { if (SocketLock.Lock(_FL)) { Socket.Reset(0); SocketLock.Unlock(); } LgiTrace("%s:%i - Failed auth.\n", _FL); } } else Error(_FL, "Failed to open socket to %s:%i and read reply.\n", Server, Port); } else Error(_FL, "No user/pass.\n"); } CleanUp: DeleteArray(Apop); DeleteArray(Server); return Status; } bool MailPop3::MailIsEnd(char *Ptr, ssize_t Len) { for (char *c = Ptr; Len-- > 0; c++) { if (*c != *Marker) { Marker = End; } if (*c == *Marker) { Marker++; if (!*Marker) { return true; } } } return false; } bool MailPop3::Receive(GArray &Trans, MailCallbacks *Callbacks) { bool Status = false; if (Trans.Length() > 0 && Socket) { for (unsigned n = 0; nIndex; GStreamI *Msg = Trans[n]->Stream; if (Msg) { int Size = 0; // Transfer is not null when the caller wants info on the bytes comming in if (Transfer || Callbacks) { // get message size sprintf_s(Buffer, sizeof(Buffer), "LIST %i\r\n", Message + 1); VERIFY_RET_VAL(Write(0, true)); VERIFY_RET_VAL(ReadReply()); char *s = strchr(Buffer, ' '); if (s) { s = strchr(s+1, ' '); if (s) { Size = atoi(s); } } } MailSrcStatus Action = DownloadAll; int TopLines = 100; if (Callbacks && Callbacks->OnSrc) { Action = Callbacks->OnSrc(Trans[n], Size, &TopLines, Callbacks->CallbackData); } if (Action == DownloadAbort) { break; } if (Action == DownloadAll || Action == DownloadTop) { if (Action == DownloadAll) { sprintf_s(Buffer, sizeof(Buffer), "RETR %i\r\n", Message + 1); } else { sprintf_s(Buffer, sizeof(Buffer), "TOP %i %i\r\n", Message + 1, TopLines); } VERIFY_RET_VAL(Write(0, true)); GLinePrefix End(".\r\n"); if (Transfer) { Transfer->Value = 0; Transfer->Range = Size; Transfer->Start = LgiCurrentTime(); } // Read status line ZeroObj(Buffer); int Used = 0; bool Ok = false; bool Finished = false; int64 DataPos = 0; while (Socket->IsOpen()) { ssize_t r = Socket->Read(Buffer+Used, sizeof(Buffer)-Used-1, 0); if (r > 0) { DeNullText(Buffer + Used, r); if (Transfer) { Transfer->Value += r; } char *Eol = strchr(Buffer, '\n'); if (Eol) { Eol++; Ok = Buffer[0] == '+'; if (Ok) { // Log(Buffer, GSocketI::SocketMsgReceive); // The Buffer was zero'd at the beginning garrenteeing // NULL termination size_t Len = strlen(Eol); ssize_t EndPos = End.IsEnd(Eol, Len); if (EndPos >= 0) { Msg->Write(Eol, EndPos - 3); Status = Trans[n]->Status = true; Finished = true; } else { Msg->Write(Eol, Len); DataPos += Len; } } else { Log(Buffer, GSocketI::SocketMsgError); Finished = true; } break; } Used += r; } else break; } if (!Finished) { if (Ok) { // Read rest of message while (Socket->IsOpen()) { ssize_t r = Socket->Read(Buffer, sizeof(Buffer), 0); if (r > 0) { DeNullText(Buffer, r); if (Transfer) { Transfer->Value += r; } ssize_t EndPos = End.IsEnd(Buffer, r); if (EndPos >= 0) { ssize_t Actual = EndPos - DataPos - 3; if (Actual > 0) { ssize_t w = Msg->Write(Buffer, Actual); LgiAssert(w == Actual); } // else the end point was in the last buffer Status = Trans[n]->Status = true; break; } else { ssize_t w = Msg->Write(Buffer, r); LgiAssert(w == r); DataPos += r; } } else { break; } } if (!Status) { LgiTrace("%s:%i - Didn't get end-of-mail marker.\n", _FL); } } else { LgiTrace("%s:%i - Didn't get Ok.\n", _FL); break; } } if (Callbacks && Callbacks->OnReceive) { Callbacks->OnReceive(Trans[n], Callbacks->CallbackData); } if (Transfer) { Transfer->Empty(); } } else { Trans[n]->Oversize = Status = true; } if (Items) { Items->Value++; } } else { LgiTrace("%s:%i - No stream.\n", _FL); } } } else { LgiTrace("%s:%i - Arg check failed, len=%p, sock=%p.\n", _FL, Trans.Length(), Socket.Get()); } return Status; } bool MailPop3::GetSizes(GArray &Sizes) { if (Socket) { strcpy_s(Buffer, sizeof(Buffer), (char*)"LIST\r\n"); VERIFY_RET_VAL(Write(0, true)); char *s = 0; if (ReadMultiLineReply(s)) { GToken l(s, "\r\n"); DeleteArray(s); for (unsigned i=0; i 0; } int MailPop3::Sizeof(int Message) { int Size = 0; if (Socket) { sprintf_s(Buffer, sizeof(Buffer), "LIST %i\r\n", Message + 1); VERIFY_RET_VAL(Write(0, true)); VERIFY_RET_VAL(ReadReply()); char *s = strchr(Buffer, ' '); if (s) { s = strchr(s+1, ' '); if (s) { Size = atoi(s); } } } return Size; } bool MailPop3::Delete(int Message) { if (Socket) { sprintf_s(Buffer, sizeof(Buffer), "DELE %i\r\n", Message + 1); VERIFY_RET_VAL(Write(0, true)); VERIFY_RET_VAL(ReadReply()); return true; } return false; } bool MailPop3::GetUid(int Index, char *Id, int IdLen) { if (Socket && Id) { sprintf_s(Buffer, sizeof(Buffer), "UIDL %i\r\n", Index + 1); VERIFY_RET_VAL(Write(0, true)); VERIFY_RET_VAL(ReadReply()); char *Space = strchr(Buffer, ' '); if (Space) { Space = strchr(Space+1, ' '); if (Space) { for (char *s = Space+1; *s; s++) { if (*s == '\r' || *s == '\n') { *s = 0; break; } } strcpy_s(Id, IdLen, Space+1); return true; } } } return false; } bool MailPop3::GetUidList(List &Id) { bool Status = false; if (Socket) { char *Str = 0; sprintf_s(Buffer, sizeof(Buffer), "UIDL\r\n"); VERIFY_RET_VAL(Write(0, true)); VERIFY_RET_VAL(ReadMultiLineReply(Str)); if (Str) { Status = true; GToken T(Str, "\r\n"); for (unsigned i=0; iRead(Buffer, sizeof(Buffer), 0); if (ReadLen > 0 && Buffer[0] == '+') { // positive response char *Eol = strchr(Buffer, '\n'); if (Eol) { char *Ptr = Eol + 1; ReadLen -= Ptr-Buffer; memmove(Buffer, Ptr, ReadLen); Temp.Write((uchar*) Buffer, ReadLen); while (!MailIsEnd(Buffer, ReadLen)) { ReadLen = Socket->Read(Buffer, sizeof(Buffer), 0); if (ReadLen > 0) { Temp.Write((uchar*) Buffer, ReadLen); } else break; } int Len = (int)Temp.GetSize(); Str = new char[Len+1]; if (Str) { Temp.Read((uchar*)Str, Len); Str[Len] = 0; Status = true; } } } } return Status; } bool MailPop3::Close() { if (Socket) { // logout VERIFY_RET_VAL(Write("QUIT\r\n", true)); // 2 sec timeout, we don't really care about the server's response Socket->SetTimeout(2000); ReadReply(); if (SocketLock.Lock(_FL)) { Socket.Reset(0); SocketLock.Unlock(); } Messages = 0; return true; } return false; } ////////////////////////////////////////////////////////////////////////////////////////////////// /* MailMessage::MailMessage() { Log = this; From = 0; Reply = 0; InternetHeader = 0; Priority = MAIL_PRIORITY_NORMAL; Text = 0; TextCharset = 0; Html = 0; HtmlCharset = 0; MarkColour = -1; DispositionNotificationTo = false; Raw = 0; } MailMessage::~MailMessage() { Empty(); DeleteObj(From); DeleteObj(Reply); DeleteObj(Raw); } int MailMessage::Write(const void *Ptr, int Size, int Flags) { LgiTrace("%.*s", Size, Ptr); return Size; } void MailMessage::Empty() { Subject.Reset(); MessageID.Reset(); References.Reset(); FwdMsgId.Reset(); BounceMsgId.Reset(); DeleteArray(InternetHeader); DeleteArray(Text); DeleteArray(TextCharset); DeleteArray(Html); DeleteArray(HtmlCharset); To.DeleteObjects(); FileDesc.DeleteObjects(); } char *MailMessage::GetBody() { return Text; } char *MailMessage::GetBodyCharset() { return TextCharset; } bool MailMessage::SetBodyCharset(const char *Cs) { DeleteArray(TextCharset); TextCharset = NewStr(Cs); return true; } bool MailMessage::SetBody(const char *Txt, int Bytes, bool Copy, const char *Cs) { if (Txt != Text) { DeleteArray(Text); Text = Copy ? NewStr(Txt, Bytes) : (char*)Txt; if (Txt && !Text) return false; } if (Cs != TextCharset) { DeleteArray(TextCharset); if (!(TextCharset = NewStr(Cs))) return false; } return true; } char *MailMessage::GetHtml() { return Html; } char *MailMessage::GetHtmlCharset() { return HtmlCharset; } bool MailMessage::SetHtmlCharset(const char *Cs) { DeleteArray(HtmlCharset); HtmlCharset = NewStr(Cs); return true; } bool MailMessage::SetHtml(const char *Txt, int Bytes, bool Copy, const char *Cs) { if (Txt != Html) { DeleteArray(Html); Html = Copy ? NewStr(Txt, Bytes) : (char*)Txt; if (Txt && !Html) return false; } if (Cs != HtmlCharset) { DeleteArray(HtmlCharset); if (!(HtmlCharset = NewStr(Cs))) return false; } return true; } int MailMessage::EncodeBase64(GStreamI &Out, GStreamI &In) { int64 Start = LgiCurrentTime(); int Status = 0; int BufSize = 4 << 10; char *InBuf = new char[BufSize]; char *OutBuf = new char[BufSize]; if (InBuf && OutBuf) { int InLen = (int)In.GetSize(); // file remaining to read int InUsed = 0; int InDone = 0; int OutUsed = 0; do { if (InUsed - InDone < 256 && InLen > 0) { // Move any bit left over down to the start memmove(InBuf, InBuf + InDone, InUsed - InDone); InUsed -= InDone; InDone = 0; // Read in as much data as we can int Max = min(BufSize-InUsed, InLen); int r = In.Read(InBuf + InUsed, Max); if (r <= 0) break; // FilePos += r; InUsed += r; InLen -= r; } if (OutUsed > BufSize - 256) { int w = Out.Write(OutBuf, OutUsed); if (w > 0) { OutUsed = 0; Status += w; } else { break; } } int OutLen = ConvertBinaryToBase64( OutBuf + OutUsed, 76, (uchar*)InBuf + InDone, InUsed - InDone); int In = OutLen * 3 / 4; InDone += In; OutUsed += OutLen; OutBuf[OutUsed++] = '\r'; OutBuf[OutUsed++] = '\n'; } while (InDone < InUsed); if (OutUsed > 0) { int w = Out.Write(OutBuf, OutUsed); if (w >= 0) Status += w; w = Out.Write((char*)"\r\n", 2); if (w >= 0) Status += w; } #if 0 double Sec = (double)((int64)LgiCurrentTime() - Start) / 1000.0; double Kb = (double)FileDes->Sizeof() / 1024.0; LgiTrace("rate: %ikb/s\n", (int)(Kb / Sec)); #endif } else Log->Print("%s:%i - Error allocating buffers\n", _FL); DeleteArray(InBuf); DeleteArray(OutBuf); return Status; } int MailMessage::EncodeQuotedPrintable(GStreamI &Out, GStreamI &In) { int Status = 0; char OutBuf[100], InBuf[1024]; int ch = 0; int InLen; // Read the input data one chunk at a time while ((InLen = In.Read(InBuf, sizeof(InBuf))) > 0) { // For all the input bytes we just got for (char *s = InBuf; s - InBuf < InLen; ) { if (*s == '\n') { ch += sprintf_s(OutBuf+ch, sizeof(OutBuf)-ch, "\r\n"); int w = Out.Write(OutBuf, ch); if (w <= 0) break; ch = 0; Status += w; } else if (*s == '.') { // If the '.' character happens to fall at the // end of a paragraph and gets pushed onto the next line it // forms the magic \r\n.\r\n sequence that ends an SMTP data // session. Which is bad. The solution taken here is to // hex encode it if it falls at the start of the line. // Otherwise allow it through unencoded. if (ch == 0) { ch += sprintf_s(OutBuf+ch, sizeof(OutBuf)-ch, "=%2.2X", (uchar)*s); } else { OutBuf[ch++] = *s; } } else if (*s & 0x80 || *s == '=') { // Require hex encoding of 8-bit chars and the equals itself. ch += sprintf_s(OutBuf+ch, sizeof(OutBuf)-ch, "=%2.2X", (uchar)*s); } else if (*s != '\r') { OutBuf[ch++] = *s; } s++; if (ch > 73) { // time for a new line. ch += sprintf_s(OutBuf+ch, sizeof(OutBuf)-ch, "=\r\n"); int w = Out.Write(OutBuf, ch); if (w <= 0) break; ch = 0; Status += w; } } } ch += sprintf_s(OutBuf+ch, sizeof(OutBuf)-ch, "\r\n"); int w = Out.Write(OutBuf, ch); if (w > 0) Status += w; return Status; } int MailMessage::EncodeText(GStreamI &Out, GStreamI &In) { int Status = 0; char InBuf[4096]; int InLen, InUsed = 0; const char *Eol = "\r\n"; while ((InLen = In.Read(InBuf+InUsed, sizeof(InBuf)-InUsed)) > 0) { InUsed += InLen; char *s; for (s = InBuf; s - InBuf < InUsed; ) { // Do we have a complete line? int RemainingBytes = InUsed - (s - InBuf); char *NewLine = strnchr(s, '\n', RemainingBytes); if (NewLine) { // Yes... write that out. int Len = NewLine - s; if (Len > 0 && s[Len-1] == '\r') Len--; if (Len == 1 && s[0] == '.') { // this removes the sequence ".\n" // which is the END OF MAIL in the SMTP protocol. int w = Out.Write((char*)". ", 2); if (w <= 0) break; Status += w; } else if (Len) { int w = Out.Write(s, Len); if (w <= 0) break; Status += w; } int w = Out.Write(Eol, 2); if (w <= 0) break; s = NewLine + 1; Status += w; } else { // No... move the data down to the start of the buffer memmove(InBuf, s, RemainingBytes); InUsed = RemainingBytes; s = 0; break; } } if (s) InUsed -= s - InBuf; } if (InUsed) { int w = Out.Write(InBuf, InUsed); if (w > 0) Status += w; w = Out.Write(Eol, 2); if (w > 0) Status += w; } return Status; } #define SEND_BUF_SIZE (16 << 10) class SendBuf : public GStream { GStreamI *Out; int Used; uchar Buf[SEND_BUF_SIZE]; public: bool Status; SendBuf(GStreamI *out) { Out = out; Used = 0; Status = true; } ~SendBuf() { Flush(); } int64 GetSize() { return Used; } int64 SetSize(int64 Size) { return Out->SetSize(Size); } int Read(void *Buffer, int Size, int Flags = 0) { return -1; } void Flush() { if (Used > 0) { int w = Out->Write(Buf, Used, 0); if (w < Used) { Status = false; } Used = 0; } } int Write(const void *Buffer, int Size, int Flags = 0) { int64 w = 0; uchar *Ptr = (uchar*)Buffer; while (Ptr && Size > 0) { if (Size + Used >= SEND_BUF_SIZE) { int Chunk = SEND_BUF_SIZE - Used; memcpy(Buf + Used, Ptr, Chunk); int s = Out->Write(Buf, SEND_BUF_SIZE, 0); if (s < SEND_BUF_SIZE) { return -1; break; } Ptr += Chunk; Size -= Chunk; w += Chunk; Used = 0; } else { memcpy(Buf + Used, Ptr, Size); Used += Size; w += Size; Size = 0; } } return (int)w; } }; // Encode the whole email bool MailMessage::Encode(GStreamI &Out, GStream *HeadersSink, MailProtocol *Protocol, bool Mime) { GStringPipe p; bool Status = EncodeHeaders(p, Protocol, Mime); if (Status) { int Len = (int)p.GetSize(); char *Headers = p.NewStr(); if (HeadersSink) { HeadersSink->Write(Headers, Len); } else { InternetHeader = NewStr(Headers); } if (Headers && Out.Write(Headers, Len)) { SendBuf *Buf = new SendBuf(&Out); if (Buf) { Status = EncodeBody(*Buf, Protocol, Mime); if (Status) { Buf->Flush(); Status = Buf->Status; if (!Status) Log->Print("%s:%i - Buffer status failed.\n", _FL); } else Log->Print("%s:%i - EncodeBody failed.\n", _FL); DeleteObj(Buf); } } else Log->Print("%s:%i - Headers output failed.\n", _FL); DeleteArray(Headers); } else Log->Print("%s:%i - EncodeHeaders failed.\n", _FL); return Status; } #define WriteOutput() \ if (Out.Write(Buffer, Len) != Len) \ { \ Log->Print("%s:%i - Write failed.\n", _FL); \ Status = false; \ } // This encodes the main headers but not the headers relating to the // actual content. Thats done by the ::EncodeBody function. bool MailMessage::EncodeHeaders(GStreamI &Out, MailProtocol *Protocol, bool Mime) { bool Status = true; // Setup char Buffer[1025]; // Construct date const char *Weekday[7] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" }; const char *Month[12] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; LDateTime Dt; int TimeZone = Dt.SystemTimeZone(); Dt.SetNow(); int Len = sprintf_s(Buffer, sizeof(Buffer), "Date: %s, %i %s %i %i:%2.2i:%2.2i %s%2.2d%2.2d\r\n", Weekday[Dt.DayOfWeek()], Dt.Day(), Month[Dt.Month()-1], Dt.Year(), Dt.Hours(), Dt.Minutes(), Dt.Seconds(), (TimeZone >= 0) ? "+" : "", TimeZone / 60, abs(TimeZone) % 60); WriteOutput(); if (Protocol && Protocol->ProgramName) { // X-Mailer: Len = sprintf_s(Buffer, sizeof(Buffer), "X-Mailer: %s\r\n", Protocol->ProgramName.Get()); WriteOutput(); } if (Protocol && Protocol->ExtraOutgoingHeaders) { for (char *s=Protocol->ExtraOutgoingHeaders; s && *s; ) { char *e = s; while (*e && *e != '\r' && *e != '\n') e++; int 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; } if (!Status) Log->Print("%s:%i - Writing ExtraOutgoingHeaders failed.\n", _FL); } if (Priority != MAIL_PRIORITY_NORMAL) { // X-Priority: Len = sprintf_s(Buffer, sizeof(Buffer), "X-Priority: %i\r\n", Priority); WriteOutput(); } if (MarkColour >= 0) { // X-Color (HTML Colour Ref for email marking) Len = sprintf_s(Buffer, sizeof(Buffer), "X-Color: #%2.2X%2.2X%2.2X\r\n", R24(MarkColour), G24(MarkColour), B24(MarkColour)); WriteOutput(); } // Message-ID: if (MessageID) { for (char *m=MessageID; *m; m++) { if (*m <= ' ') { printf("%s:%i - Bad message ID '%s'\n", _FL, MessageID.Get()); return false; } } Len = sprintf_s(Buffer, sizeof(Buffer), "Message-ID: %s\r\n", MessageID.Get()); WriteOutput(); } // References: if (ValidStr(References)) { char *Dir = strrchr(References, '/'); GAutoString a; char *Ref = 0; if (Dir) { GUri u; a = u.Decode(Dir + 1); Ref = a; } else Ref = References; int Len = sprintf_s(Buffer, sizeof(Buffer), "References: <%s>\r\n", Ref); WriteOutput(); } // To: char *ToAddr = CreateAddressTag(To, 0, &Protocol->CharsetPrefs); if (ToAddr) { Status &= Out.Write(ToAddr, strlen(ToAddr)) > 0; DeleteArray(ToAddr); if (!Status) Log->Print("%s:%i - Writing ToAddr failed.\n", _FL); } char *CcAddr = CreateAddressTag(To, 1, &Protocol->CharsetPrefs); if (CcAddr) { Status &= Out.Write(CcAddr, strlen(CcAddr)) > 0; DeleteArray(CcAddr); if (!Status) Log->Print("%s:%i - Writing CcAddr failed.\n", _FL); } // From: if (From && From->Addr) { Len = sprintf_s(Buffer, sizeof(Buffer), "From: "); char *Nme = EncodeRfc2047(NewStr(From->Name), 0, &Protocol->CharsetPrefs); if (Nme) { if (strchr(Nme, '\"')) Len += sprintf_s(Buffer+Len, sizeof(Buffer)-Len, "'%s' ", Nme); else Len += sprintf_s(Buffer+Len, sizeof(Buffer)-Len, "\"%s\" ", Nme); DeleteArray(Nme); } Len += sprintf_s(Buffer+Len, sizeof(Buffer)-Len, "<%s>\r\n", From->Addr); WriteOutput(); } else { Log->Print("%s:%i - No 'from' address to send email.\n", _FL); return false; } // Reply-To: if (Reply && ValidStr(Reply->Addr)) { Len = sprintf_s(Buffer, sizeof(Buffer), "Reply-To: "); char *Nme = EncodeRfc2047(NewStr(Reply->Name), 0, &Protocol->CharsetPrefs); if (Nme) { if (strchr(Nme, '\"')) Len += sprintf_s(Buffer+Len, sizeof(Buffer)-Len, "'%s' ", Nme); else Len += sprintf_s(Buffer+Len, sizeof(Buffer)-Len, "\"%s\" ", Nme); DeleteArray(Nme); } Len += sprintf_s(Buffer+Len, sizeof(Buffer)-Len, "<%s>\r\n", Reply->Addr); WriteOutput(); } // Subject: char *Subj = EncodeRfc2047(NewStr(Subject), 0, &Protocol->CharsetPrefs, 9); Len = sprintf_s(Buffer, sizeof(Buffer), "Subject: %s\r\n", (Subj) ? Subj : ""); WriteOutput(); DeleteArray(Subj); // DispositionNotificationTo if (DispositionNotificationTo) { Len = sprintf_s(Buffer, sizeof(Buffer), "Disposition-Notification-To:"); char *Nme = EncodeRfc2047(NewStr(From->Name), 0, &Protocol->CharsetPrefs); if (Nme) { Len += sprintf_s(Buffer+Len, sizeof(Buffer)-Len, " \"%s\"", Nme); DeleteArray(Nme); } Len += sprintf_s(Buffer+Len, sizeof(Buffer)-Len, " <%s>\r\n", From->Addr); WriteOutput(); } return Status; } bool MailMessage::EncodeBody(GStreamI &Out, MailProtocol *Protocol, bool Mime) { bool Status = true; char Buffer[1025]; if (Mime) { bool MultiPart = ((Text ? 1 : 0) + (Html ? 1 : 0) + FileDesc.Length()) > 1; bool MultipartAlternate = ValidStr(Text) && ValidStr(Html); bool MultipartMixed = FileDesc.Length() > 0; uint64 Now = LgiCurrentTime(); char Separator[256]; sprintf_s(Separator, sizeof(Separator), "----=_NextPart_%8.8X.%8.8X", (uint32)Now, (unsigned)(int64)LgiGetCurrentThread()); int Len = sprintf_s(Buffer, sizeof(Buffer), "MIME-Version: 1.0\r\n"); Status &= Out.Write(Buffer, Len) > 0; if (MultiPart) { const char *Type = MultipartMixed ? EncryptedMsg ? sMultipartEncrypted : sMultipartMixed : sMultipartAlternative; Len = sprintf_s(Buffer, sizeof(Buffer), "Content-Type: %s;\r\n\tboundary=\"%s\"\r\n", Type, Separator); Status &= Out.Write(Buffer, Len) > 0; } if (ValidStr(Text) || ValidStr(Html)) { char AlternateBoundry[128] = ""; if (MultiPart) { Len = sprintf_s(Buffer, sizeof(Buffer), "\r\n--%s\r\n", Separator); Status &= Out.Write(Buffer, Len) > 0; if (MultipartMixed && MultipartAlternate) { sprintf_s(AlternateBoundry, sizeof(AlternateBoundry), "----=_NextPart_%8.8X.%8.8X", (uint32)++Now, (uint32)(int64)LgiGetCurrentThread()); Len = sprintf_s(Buffer, sizeof(Buffer), "Content-Type: %s;\r\n\tboundary=\"%s\"\r\n", sMultipartAlternative, AlternateBoundry); Status &= Out.Write(Buffer, Len) > 0; Len = sprintf_s(Buffer, sizeof(Buffer), "\r\n--%s\r\n", AlternateBoundry); Status &= Out.Write(Buffer, Len) > 0; } } if (!Status) Log->Print("%s:%i - Status=%i\n", _FL, Status); if (ValidStr(Text)) { const char *Cs = 0; char *Txt = Text, *Mem = 0; // Detect charset if (!TextCharset || _stricmp(TextCharset, "utf-8") == 0) { Cs = LgiDetectCharset(Text, -1, Protocol ? &Protocol->CharsetPrefs : 0); if (Cs) { Mem = Txt = (char*)LgiNewConvertCp(Cs, Text, "utf-8", -1); } } if (!Cs) Cs = TextCharset; // Content type Len = sprintf_s(Buffer, sizeof(Buffer), "Content-Type: text/plain; charset=\"%s\"\r\n", Cs ? Cs : "us-ascii"); Status &= Out.Write(Buffer, Len) > 0; if (!Status) Log->Print("%s:%i - Status=%i\n", _FL, Status); // Transfer encoding if (Txt && (Is8Bit(Txt) || MaxLineLen(Txt) >= 80)) { char QuotPrint[] = "Content-Transfer-Encoding: quoted-printable\r\n\r\n"; Status &= Out.Write(QuotPrint, strlen(QuotPrint)) > 0; if (!Status) Log->Print("%s:%i - Status=%i\n", _FL, Status); GMemStream TxtStr(Txt, strlen(Txt), false); Status &= EncodeQuotedPrintable(Out, TxtStr) > 0; if (!Status) Log->Print("%s:%i - Status=%i\n", _FL, Status); } else { char Cte[] = "Content-Transfer-Encoding: 7bit\r\n\r\n"; Status &= Out.Write(Cte, strlen(Cte)) > 0; if (!Status) Log->Print("%s:%i - Status=%i\n", _FL, Status); GMemStream TxtStr(Txt, strlen(Txt), false); Status &= EncodeText(Out, TxtStr) > 0; if (!Status) Log->Print("%s:%i - Status=%i\n", _FL, Status); } DeleteArray(Mem); } if (!Status) Log->Print("%s:%i - Status=%i\n", _FL, Status); // Break alternate part if (AlternateBoundry[0]) { Len = sprintf_s(Buffer, sizeof(Buffer), "--%s\r\n", AlternateBoundry); Status &= Out.Write(Buffer, Len) > 0; } else if (MultipartAlternate) { Len = sprintf_s(Buffer, sizeof(Buffer), "--%s\r\n", Separator); Status &= Out.Write(Buffer, Len) > 0; } if (ValidStr(Html)) { // Content type Len = sprintf_s(Buffer, sizeof(Buffer), "Content-Type: text/html; charset=\"%s\"\r\n", TextCharset ? TextCharset : "us-ascii"); Status &= Out.Write(Buffer, Len) > 0; // Transfer encoding if (Is8Bit(Html) || MaxLineLen(Html) >= 80) { char Qp[] = "Content-Transfer-Encoding: quoted-printable\r\n\r\n"; Status &= Out.Write(Qp, strlen(Qp)) > 0; GMemStream HtmlStr(Html, strlen(Html), false); Status &= EncodeQuotedPrintable(Out, HtmlStr) > 0; } else { char Sb[] = "Content-Transfer-Encoding: 7bit\r\n\r\n"; Status &= Out.Write(Sb, strlen(Sb)) > 0; GMemStream HtmlStr(Html, strlen(Html), false); Status &= EncodeText(Out, HtmlStr) > 0; } } if (!Status) Log->Print("%s:%i - Status=%i\n", _FL, Status); if (AlternateBoundry[0]) { // End alternate part Len = sprintf_s(Buffer, sizeof(Buffer), "\r\n--%s--\r\n", AlternateBoundry); Status &= Out.Write(Buffer, Len) > 0; } } if (!Status) Log->Print("%s:%i - Status=%i\n", _FL, Status); int SizeEst = 1024; FileDescriptor *FileDes = FileDesc.First(); for (; FileDes; FileDes=FileDesc.Next()) { SizeEst += FileDes->Sizeof() * 4 / 3; } Out.SetSize(SizeEst); FileDes = FileDesc.First(); while (FileDes) { GStreamI *F = FileDes->GotoObject(); // write a MIME segment for this attachment if (MultiPart) { Len = sprintf_s(Buffer, sizeof(Buffer), "--%s\r\n", Separator); Status &= Out.Write(Buffer, Len) > 0; } char FileName[256]; char *s = FileDes->Name(), *d = FileName; if (!s) Log->Print("%s:%i - File descriptor has no name.\n", _FL); else { while (*s) { if (*s != '\\') { *d++ = *s++; } else { *d++ = '\\'; *d++ = '\\'; s++; } } *d = 0; char *FName = EncodeRfc2047(NewStr(FileName), 0, &Protocol->CharsetPrefs); Len = sprintf_s(Buffer, sizeof(Buffer), "Content-Type: %s; name=\"%s\"\r\n" "Content-Disposition: attachment\r\n", FileDes->GetMimeType() ? FileDes->GetMimeType() : "application/x-zip-compressed", (FName) ? FName : FileName); Status &= Out.Write(Buffer, Len) > 0; DeleteArray(FName); if (FileDes->GetContentId()) { Len = sprintf_s(Buffer, sizeof(Buffer), "Content-Id: %s\r\n", FileDes->GetContentId()); Status &= Out.Write(Buffer, Len) > 0; } Len = sprintf_s(Buffer, sizeof(Buffer), "Content-Transfer-Encoding: base64\r\n\r\n"); Status &= Out.Write(Buffer, Len) > 0; Status &= F ? EncodeBase64(Out, *F) > 0 : false; } FileDes = FileDesc.Next(); } if (!Status) Log->Print("%s:%i - Status=%i\n", _FL, Status); if (MultiPart) { // write final separator Len = sprintf_s(Buffer, sizeof(Buffer), "--%s--\r\n\r\n", Separator); Status &= Out.Write(Buffer, Len) > 0; } } else { // send content type int Len = sprintf_s(Buffer, sizeof(Buffer), "Content-Type: text/plain; charset=\"%s\"\r\n", TextCharset ? TextCharset : "us-ascii"); Status &= Out.Write(Buffer, Len) > 0; if (Is8Bit(Text)) { // send the encoding and a blank line char Qp[] = "Content-Transfer-Encoding: quoted-printable\r\n\r\n"; Status &= Out.Write(Qp, strlen(Qp)) > 0; // send message text GMemStream TextStr(Text, strlen(Text), false); Status &= EncodeQuotedPrintable(Out, TextStr) > 0; } else { // send a blank line Status &= Out.Write((char*)"\r\n", 2) > 0; // send message text GMemStream TextStr(Text, strlen(Text), false); Status &= EncodeText(Out, TextStr) > 0; } } if (!Status) Log->Print("%s:%i - Status=%i\n", _FL, Status); return true; } */ diff --git a/src/common/INet/MailImap.cpp b/src/common/INet/MailImap.cpp --- a/src/common/INet/MailImap.cpp +++ b/src/common/INet/MailImap.cpp @@ -1,3579 +1,3298 @@ #include #ifdef LINUX #include #endif #include "Lgi.h" #include "GToken.h" #include "Mail.h" #include "Base64.h" #include "INetTools.h" #include "GDocView.h" #include "IHttp.h" #include "HttpTools.h" #include "OpenSSLSocket.h" #include "LJson.h" #define DEBUG_OAUTH2 0 #ifdef _DEBUG #define DEBUG_FETCH 0 #else #define DEBUG_FETCH 0 #endif #define OPT_ImapOAuth2AccessToken "OAuth2AccessTok" #undef _FL #define _FL LgiGetLeaf(__FILE__), __LINE__ //////////////////////////////////////////////////////////////////////////// #if GPL_COMPATIBLE #include "AuthNtlm/Ntlm.h" #else #include "../../src/common/INet/libntlm-0.4.2/ntlm.h" #endif #if HAS_LIBGSASL #include "gsasl.h" #endif static const char *sRfc822Header = "RFC822.HEADER"; static const char *sRfc822Size = "RFC822.SIZE"; -bool Base64Str(GString &s) +struct TraceLog : public GStream { - GString b64; - ssize_t Base64Len = BufferLen_BinTo64(s.Length()); - if (!b64.Set(NULL, Base64Len)) - return false; - - ssize_t Ch = ConvertBinaryToBase64(b64.Get(), b64.Length(), (uchar*)s.Get(), s.Length()); - LgiAssert(Ch == b64.Length()); - s = b64; - return true; -} - -bool UnBase64Str(GString &s) -{ - GString Bin; - ssize_t BinLen = BufferLen_64ToBin(s.Length()); - if (!Bin.Set(NULL, BinLen)) - return false; - - ssize_t Ch = ConvertBase64ToBinary((uchar*)Bin.Get(), Bin.Length(), s.Get(), s.Length()); - LgiAssert(Ch <= (int)Bin.Length()); - s = Bin; - s.Get()[Ch] = 0; - return true; -} + ssize_t Read(void *Buffer, ssize_t Size, int Flags = 0) { return 0; } + ssize_t Write(const void *Buffer, ssize_t Size, int Flags = 0) + { + LgiTrace("%.*s", (int)Size, Buffer); + return Size; + } +}; /* #define SkipWhiteSpace(s) while (*s && IsWhiteSpace(*s)) s++; bool JsonDecode(GXmlTag &t, const char *s) { if (*s != '{') return false; s++; while (*s) { SkipWhiteSpace(s); if (*s != '\"') break; GAutoString Variable(LgiTokStr(s)); SkipWhiteSpace(s); if (*s != ':') return false; s++; SkipWhiteSpace(s); GAutoString Value(LgiTokStr(s)); SkipWhiteSpace(s); t.SetAttr(Variable, Value); if (*s != ',') break; s++; } if (*s != '}') return false; s++; return true; } */ #define SkipWhite(s) while (*s && strchr(WhiteSpace, *s)) s++ #define SkipSpaces(s) while (*s && strchr(" \t", *s)) s++ #define SkipNonWhite(s) while (*s && !strchr(WhiteSpace, *s)) s++; #define ExpectChar(ch) if (*s != ch) return 0; s++ ssize_t MailIMap::ParseImapResponse(char *Buffer, ssize_t BufferLen, GArray &Ranges, int Names) { Ranges.Length(0); if (!*Buffer || *Buffer != '*') return 0; char *End = Buffer + BufferLen; char *s = Buffer + 1; char *Start; for (int n=0; nOpen(u.Host, u.Port?u.Port:443)) return false; ssize_t w = S->Write(Req, ReqLen); if (w != ReqLen) return false; char Buf[256]; GArray Res; ssize_t r; ssize_t ContentLen = 0; ssize_t HdrLen = 0; while ((r = S->Read(Buf, sizeof(Buf))) > 0) { ssize_t Old = Res.Length(); Res.Length(Old + r); memcpy(&Res[Old], Buf, r); if (ContentLen) { if (Res.Length() >= HdrLen + ContentLen) break; } else { char *Eoh = strnstr(&Res[0], "\r\n\r\n", Res.Length()); if (Eoh) { HdrLen = Eoh - &Res[0]; GAutoString c(InetGetHeaderField(&Res[0], "Content-Length", HdrLen)); if (c) { ContentLen = atoi(c); } } } } char *Rp = &Res[0]; char *Eoh = strnstr(Rp, "\r\n\r\n", Res.Length()); if (Eoh) { if (OutHeaders) OutHeaders->Reset(NewStr(Rp, Eoh-Rp)); if (OutBody) OutBody->Reset(NewStr(Eoh + 4, Res.Length() - (Eoh-Rp) - 4)); if (StatusCode) { *StatusCode = 0; char *Eol = strchr(Rp, '\n'); if (Eol) { GToken t(Rp, " \t\r\n", true, Eol - Rp); if (t.Length() > 2) { *StatusCode = atoi(t[1]); } } } } else return false; #ifndef _DEBUG GFile f; if (f.Open("c:\\temp\\http.html", O_WRITE)) { f.SetSize(0); f.Write(&Res[0], Res.Length()); f.Close(); } #endif return true; } GAutoString ImapBasicTokenize(char *&s) { if (s) { while (*s && strchr(WhiteSpace, *s)) s++; char start = 0, end = 0; if (*s == '\'' || *s == '\"') start = end = *s; else if (*s == '[') { start = '['; end = ']'; } else if (*s == '(') { start = '('; end = ')'; } else if (*s == '{') { start = '{'; end = '}'; } if (start && end) { s++; char *e = strchr(s, end); if (e) { char *n = NewStr(s, e - s); s = e + 1; return GAutoString(n); } } else { char *e = s; while (*e && !strchr(WhiteSpace, *e)) e++; if (e > s) { char *n = NewStr(s, e - s); s = e + (*e != 0); return GAutoString(n); } } } s += strlen(s); return GAutoString(); } char *Tok(char *&s) { char *Ret = 0; while (*s && strchr(WhiteSpace, *s)) s++; if (*s == '=' || *s == ',') { Ret = NewStr(s++, 1); } else if (*s == '\'' || *s == '\"') { char d = *s++; char *e = strchr(s, d); if (e) { Ret = NewStr(s, e - s); s = e + 1; } } else if (*s) { char *e; for (e=s; *e && (IsDigit(*e) || IsAlpha(*e) || *e == '-'); e++); Ret = NewStr(s, e - s); s = e; } return Ret; } char *DecodeImapString(const char *s) { GStringPipe p; while (s && *s) { if (*s == '&') { char Escape = *s++; const char *e = s; while (*e && *e != '-') { e++; } ssize_t Len = e - s; if (Len) { char *Base64 = new char[Len + 4]; if (Base64) { memcpy(Base64, s, Len); char *n = Base64 + Len; for (ssize_t i=Len; i%4; i++) *n++ = '='; *n++ = 0; Len = strlen(Base64); ssize_t BinLen = BufferLen_64ToBin(Len); uint16 *Bin = new uint16[(BinLen/2)+1]; if (Bin) { BinLen = ConvertBase64ToBinary((uchar*)Bin, BinLen, Base64, Len); if (BinLen) { ssize_t Chars = BinLen / 2; Bin[Chars] = 0; for (int i=0; i>8) | ((Bin[i]&0xff)<<8); } char *c8 = WideToUtf8((char16*)Bin, BinLen); if (c8) { p.Push(c8); DeleteArray(c8); } } DeleteArray(Bin); } DeleteArray(Base64); } } else { p.Push(&Escape, 1); } s = e + 1; } else { p.Push(s, 1); s++; } } return p.NewStr(); } char *EncodeImapString(const char *s) { GStringPipe p; ssize_t Len = s ? strlen(s) : 0; while (s && *s) { int c = LgiUtf8To32((uint8*&)s, Len); DoNextChar: if ((c >= ' ' && c < 0x80) || c == '\n' || c == '\t' || c == '\r') { // Literal char ch = c; p.Push(&ch, 1); } else { // Encoded GArray Str; Str[0] = c; while ((c = LgiUtf8To32((uint8*&)s, Len))) { if ((c >= ' ' && c < 0x80) || c == '\n' || c == '\t' || c == '\r') { break; } else { Str[Str.Length()] = c; } } for (uint32 i=0; i>8) | ((Str[i]&0xff)<<8); } ssize_t BinLen = Str.Length() << 1; ssize_t BaseLen = BufferLen_BinTo64(BinLen); char *Base64 = new char[BaseLen+1]; if (Base64) { ssize_t Bytes = ConvertBinaryToBase64(Base64, BaseLen, (uchar*)&Str[0], BinLen); while (Bytes > 0 && Base64[Bytes-1] == '=') { Base64[Bytes-1] = 0; Bytes--; } Base64[Bytes] = 0; p.Print("&%s-", Base64); DeleteArray(Base64); } goto DoNextChar; } } return p.NewStr(); } void ChopNewLine(char *Str) { char *End = Str+strlen(Str)-1; if (*End == '\n') { *End-- = 0; } if (*End == '\r') { *End-- = 0; } } MailImapFolder::MailImapFolder() { Sep = '/'; Path = 0; NoSelect = false; NoInferiors = false; Marked = false; Exists = -1; Recent = -1; // UnseenIndex = -1; Deleted = 0; } MailImapFolder::~MailImapFolder() { DeleteArray(Path); } void MailImapFolder::operator =(LHashTbl,int> &v) { int o = v.Find("exists"); if (o >= 0) Exists = o; o = v.Find("recent"); if (o >= 0) Recent = o; } char *MailImapFolder::GetPath() { return Path; } void MailImapFolder::SetPath(const char *s) { char *NewPath = DecodeImapString(s); DeleteArray(Path); Path = NewPath; } char *MailImapFolder::GetName() { if (Path) { char *s = strrchr(Path, Sep); if (s) { return s + 1; } else { return Path; } } return 0; } void MailImapFolder::SetName(const char *s) { if (s) { char Buf[256]; strcpy_s(Buf, sizeof(Buf), Path?Path:(char*)""); DeleteArray(Path); char *Last = strrchr(Buf, Sep); if (Last) { Last++; strcpy_s(Last, sizeof(Buf)-(Last-Buf), s); Path = NewStr(Buf); } else { Path = NewStr(s); } } } ///////////////////////////////////////////// class MailIMapPrivate : public LMutex { public: int NextCmd; bool Logging; bool ExpungeOnExit; char FolderSep; char *Current; char *Flags; LHashTbl,bool> Capability; GString WebLoginUri; - MailIMap::OAuthParams OAuth; GViewI *ParentWnd; LCancel *Cancel; OsThread InCommand; GString LastWrite; MailIMapPrivate() : LMutex("MailImapSem") { ParentWnd = NULL; FolderSep = '/'; NextCmd = 1; Logging = true; ExpungeOnExit = true; Current = 0; Flags = 0; InCommand = 0; Cancel = NULL; } ~MailIMapPrivate() { DeleteArray(Current); DeleteArray(Flags); } }; MailIMap::MailIMap() { d = new MailIMapPrivate; Buffer[0] = 0; } MailIMap::~MailIMap() { if (Lock(_FL)) { ClearDialog(); ClearUid(); DeleteObj(d); } } bool MailIMap::Lock(const char *file, int line) { if (!d->Lock(file, line)) return false; return true; } bool MailIMap::LockWithTimeout(int Timeout, const char *file, int line) { if (!d->LockWithTimeout(Timeout, file, line)) return false; return true; } void MailIMap::Unlock() { d->Unlock(); d->InCommand = 0; } void MailIMap::SetCancel(LCancel *Cancel) { d->Cancel = Cancel; } void MailIMap::SetParentWindow(GViewI *wnd) { d->ParentWnd = wnd; } -void MailIMap::SetOAuthParams(OAuthParams &p) -{ - d->OAuth = p; -} - const char *MailIMap::GetWebLoginUri() { return d->WebLoginUri; } bool MailIMap::IsOnline() { return Socket ? Socket->IsOpen() : 0; } char MailIMap::GetFolderSep() { return d->FolderSep; } char *MailIMap::GetCurrentPath() { return d->Current; } bool MailIMap::GetExpungeOnExit() { return d->ExpungeOnExit; } void MailIMap::SetExpungeOnExit(bool b) { d->ExpungeOnExit = b; } void MailIMap::ClearUid() { if (Lock(_FL)) { Uid.DeleteArrays(); Unlock(); } } void MailIMap::ClearDialog() { if (Lock(_FL)) { Dialog.DeleteArrays(); Unlock(); } } bool MailIMap::WriteBuf(bool ObsurePass, const char *Buffer, bool Continuation) { if (Socket) { if (!Buffer) Buffer = Buf; ssize_t Len = strlen(Buffer); d->LastWrite = Buffer; if (!Continuation && d->InCommand) { GString Msg; Msg.Printf("%s:%i - WriteBuf failed(%s)\n", LgiGetLeaf(__FILE__), __LINE__, d->LastWrite.Strip().Get()); Socket->OnInformation(Msg); LgiAssert(!"Can't be issuing new commands while others are still running."); return false; } /* else { GString Msg; Msg.Printf("%s:%i - WriteBuf ok(%s)\n", LgiGetLeaf(__FILE__), __LINE__, d->LastWrite.Strip().Get()); Socket->OnInformation(Msg); } */ if (Socket->Write((void*)Buffer, Len, 0) == Len) { if (ObsurePass) { char *Sp = (char*)strrchr(Buffer, ' '); if (Sp) { Sp++; GString s; s.Printf("%.*s********\r\n", Sp - Buffer, Buffer); Log(s.Get(), GSocketI::SocketMsgSend); } } else Log(Buffer, GSocketI::SocketMsgSend); d->InCommand = LgiGetCurrentThread(); return true; } else Log("Failed to write data to socket.", GSocketI::SocketMsgError); } else Log("Not connected.", GSocketI::SocketMsgError); return false; } bool MailIMap::Read(GStreamI *Out, int Timeout) { int Lines = 0; while (!Lines && Socket) { ssize_t r = Socket->Read(Buffer, sizeof(Buffer)); if (Timeout > 0 && Socket->IsOpen() && r <= 0) { auto St = LgiCurrentTime(); auto Rd = Socket->IsReadable(Timeout); auto End = LgiCurrentTime(); if (Rd) { r = Socket->Read(Buffer, sizeof(Buffer)); if (r < 0) { Socket->Close(); LgiTrace("%s:%i - Wut? IsReadable/Read mismatch.\n", _FL); return false; } } else { if (End - St < Timeout - 20) LgiTrace("%s:%i - IsReadable broken (again)\n", _FL); return false; } } if (r > 0) { ReadBuf.Push(Buffer, r); while (ReadBuf.Pop(Buffer, sizeof(Buffer))) { // Trim trailing whitespace char *e = Buffer + strlen(Buffer) - 1; while (e > Buffer && strchr(WhiteSpace, *e)) *e-- = 0; Lines++; if (Out) { Out->Write(Buffer, strlen(Buffer)); Out->Write((char*)"\r\n", 2); } else { Dialog.Add(NewStr(Buffer)); } } } else break; } return Lines > 0; } bool MailIMap::IsResponse(const char *Buf, int Cmd, bool &Ok) { char Num[8]; int Ch = sprintf_s(Num, sizeof(Num), "A%4.4i ", Cmd); if (!Buf || _strnicmp(Buf, Num, Ch) != 0) return false; Ok = _strnicmp(Buf+Ch, "OK", 2) == 0; if (!Ok) SetError(L_ERROR_GENERIC, "Error: %s", Buf+Ch); return true; } bool MailIMap::ReadResponse(int Cmd, bool Plus) { bool Done = false; bool Status = false; if (Socket) { ssize_t Pos = Dialog.Length(); while (!Done) { if (Read(NULL)) { for (char *Dlg = Dialog[Pos]; !Done && Dlg; Dlg = Dialog.Next()) { Pos++; if (Cmd < 0 || (Plus && *Dlg == '+')) { Status = Done = true; } if (IsResponse(Dlg, Cmd, Status)) Done = true; if (d->Logging) { bool Good = strchr("*+", *Dlg) != NULL || Status; Log(Dlg, Good ? GSocketI::SocketMsgReceive : GSocketI::SocketMsgError); } } } else { // LgiTrace("%s:%i - 'Read' failed.\n", _FL); break; } } } return Status; } void Hex(char *Out, int OutLen, uchar *In, ssize_t InLen = -1) { if (Out && In) { if (InLen < 0) InLen = strlen((char*)In); for (int i=0; i 0) { Out += ch; OutLen -= ch; } else break; } } } void _unpack(void *ptr, int ptrsize, char *b64) { ConvertBase64ToBinary((uchar*) ptr, ptrsize, b64, strlen(b64)); } bool MailIMap::ReadLine() { int Len = 0; Buf[0] = 0; do { ssize_t r = Socket->Read(Buf+Len, sizeof(Buf)-Len); if (r < 1) return false; Len += r; } while (!stristr(Buf, "\r\n")); Log(Buf, GSocketI::SocketMsgReceive); return true; } #if HAS_LIBGSASL int GsaslCallback(Gsasl *ctx, Gsasl_session *sctx, Gsasl_property prop) { return 0; } #endif class OAuthWebServer : public LThread, public LMutex { bool Loop; int Port; GSocket Listen; GAutoString Req; GString Resp; bool Finished; public: OAuthWebServer(int DesiredPort = 0) : LThread("OAuthWebServerThread"), LMutex("OAuthWebServerMutex") { Loop = false; if (Listen.Listen(DesiredPort)) { Port = Listen.GetLocalPort(); Run(); } else Port = 0; Finished = false; } ~OAuthWebServer() { if (Loop) { Loop = false; while (!IsExited()) LgiSleep(10); } } int GetPort() { return Port; } GString GetRequest(LCancel *Loop, uint64 TimeoutMs = 0) { GString r; uint64 Start = LgiCurrentTime(); while (!r && (!Loop || !Loop->IsCancelled())) { if (Lock(_FL)) { if (Req) r = Req; Unlock(); } if (TimeoutMs) { uint64 Now = LgiCurrentTime(); if (Now - Start >= TimeoutMs) break; } if (!r) LgiSleep(50); } return r; } void SetResponse(const char *r) { if (Lock(_FL)) { Resp = r; Unlock(); } } bool IsFinished() { return Finished; } int Main() { GAutoPtr s; Loop = true; while (Loop) { if (Listen.CanAccept(100)) { s.Reset(new GSocket); if (!Listen.Accept(s)) s.Reset(); else { GArray Mem; ssize_t r; char buf[512]; do { r = s->Read(buf, sizeof(buf)); if (r > 0) { Mem.Add(buf, r); bool End = strnstr(&Mem[0], "\r\n\r\n", Mem.Length()) != NULL; if (End) break; } } while (r > 0); if (Lock(_FL)) { Mem.Add(0); Req.Reset(Mem.Release()); Unlock(); } // Wait for the response... GString Response; do { if (Lock(_FL)) { if (Resp) Response = Resp; Unlock(); } if (!Response) LgiSleep(10); } while (Loop && !Response); if (Response) s->Write(Response, Response.Length()); Loop = false; } } else LgiSleep(10); } Finished = true; return 0; } }; static void AddIfMissing(GArray &Auths, const char *a, GString *DefaultAuthType = NULL) { for (unsigned i=0; iLastWrite.Strip().Get()); Socket->OnInformation(Msg); */ d->InCommand = 0; d->LastWrite.Empty(); } -bool MailIMap::Open(GSocketI *s, const char *RemoteHost, int Port, const char *User, const char *Password, GDom *SettingStore, int Flags) +bool MailIMap::Open(GSocketI *s, const char *RemoteHost, int Port, const char *User, const char *Password, GDom *settingStore, int Flags) { bool Status = false; + + if (settingStore) + SettingStore = settingStore; if (SocketLock.Lock(_FL)) { Socket.Reset(s); SocketLock.Unlock(); } if (Socket && ValidStr(RemoteHost) && ValidStr(User) && ( ValidStr(Password) || - d->OAuth.IsValid() + OAuth2.IsValid() ) && Lock(_FL)) { // prepare address if (Port < 1) { if (Flags & MAIL_SSL) Port = IMAP_SSL_PORT; else Port = IMAP_PORT; } char Remote[256]; strcpy_s(Remote, sizeof(Remote), RemoteHost); char *Colon = strchr(Remote, ':'); if (Colon) { *Colon++ = 0; Port = atoi(Colon); } // Set SSL mode GVariant v; if (Flags == MAIL_SSL) v = "SSL"; Socket->SetValue(GSocket_Protocol, v); // connect if (Socket->Open(Remote, Port)) { bool IMAP4Server = false; GArray Auths; // check capability int CapCmd = d->NextCmd++; sprintf_s(Buf, sizeof(Buf), "A%4.4i CAPABILITY\r\n", CapCmd); if (WriteBuf()) { bool Rd = ReadResponse(CapCmd); CommandFinished(); if (Rd) { for (char *r=Dialog.First(); r; r=Dialog.Next()) { GToken T(r, " "); if (T.Length() > 1 && _stricmp(T[1], "CAPABILITY") == 0) { for (unsigned i=2; i 0) { Auths.DeleteAt(n); Auths.AddAt(0, DefaultAuthType); break; } } // SSL bool TlsError = false; if (TestFlag(Flags, MAIL_USE_STARTTLS)) { int CapCmd = d->NextCmd++; sprintf_s(Buf, sizeof(Buf), "A%4.4i STARTTLS\r\n", CapCmd); if (WriteBuf()) { bool Rd = ReadResponse(CapCmd); CommandFinished(); if (Rd) { GVariant v; TlsError = !Socket->SetValue(GSocket_Protocol, v="SSL"); } else { TlsError = true; } } else LgiAssert(0); if (TlsError) { Log("STARTTLS failed", GSocketI::SocketMsgError); } } // login bool LoggedIn = false; char AuthTypeStr[256] = ""; for (unsigned i=0; i 0) { strconcat(AuthTypeStr, ", "); } strconcat(AuthTypeStr, AuthType); } // Do auth #if HAS_LIBGSASL if (!_stricmp(AuthType, "GSSAPI")) { int AuthCmd = d->NextCmd++; sprintf_s(Buf, sizeof(Buf), "A%04.4i AUTHENTICATE GSSAPI\r\n", AuthCmd); if (WriteBuf() && ReadLine() && Buf[0] == '+') { // Start GSSAPI Gsasl *ctx = NULL; Gsasl_session *sess = NULL; int rc = gsasl_init(&ctx); if (rc == GSASL_OK) { char *mechs; rc = gsasl_client_mechlist(ctx, &mechs); gsasl_callback_set(ctx, GsaslCallback); rc = gsasl_client_start(ctx, AuthType, &sess); if (rc != GSASL_OK) { Log("gsasl_client_start failed", GSocketI::SocketMsgError); } // gsasl_step(ctx, gsasl_done(ctx); } else Log("gsasl_init failed", GSocketI::SocketMsgError); } else Log("AUTHENTICATE GSSAPI failed", GSocketI::SocketMsgError); } else #endif if (_stricmp(AuthType, "LOGIN") == 0 || _stricmp(AuthType, "OTP") == 0) { // clear text authentication int AuthCmd = d->NextCmd++; sprintf_s(Buf, sizeof(Buf), "A%4.4i LOGIN %s %s\r\n", AuthCmd, User, Password); if (WriteBuf(true)) { LoggedIn = ReadResponse(AuthCmd); CommandFinished(); } } else if (_stricmp(AuthType, "PLAIN") == 0) { // plain auth type char s[256]; char *e = s; *e++ = 0; strcpy_s(e, sizeof(s)-(e-s), User); e += strlen(e); e++; strcpy_s(e, sizeof(s)-(e-s), Password); e += strlen(e); *e++ = '\r'; *e++ = '\n'; ssize_t Len = e - s - 2; int AuthCmd = d->NextCmd++; sprintf_s(Buf, sizeof(Buf), "A%4.4i AUTHENTICATE PLAIN\r\n", AuthCmd); if (WriteBuf()) { if (ReadResponse(AuthCmd, true)) { ssize_t b = ConvertBinaryToBase64(Buf, sizeof(Buf), (uchar*)s, Len); strcpy_s(Buf+b, sizeof(Buf)-b, "\r\n"); if (WriteBuf(false, NULL, true)) { bool Rd = ReadResponse(AuthCmd); CommandFinished(); if (Rd) { LoggedIn = true; } else { // Look for WEBALERT from Google for (char *s = Dialog.First(); s; s = Dialog.Next()) { char *start = strchr(s, '['); char *end = start ? strrchr(start, ']') : NULL; if (start && end) { start++; if (_strnicmp(start, "WEBALERT", 8) == 0) { start += 8; while (*start && strchr(WhiteSpace, *start)) start++; d->WebLoginUri.Set(start, end - start); } } } } } } } } #if (GPL_COMPATIBLE || defined(_LIBNTLM_H)) && defined(WINNATIVE) else if (_stricmp(AuthType, "NTLM") == 0) { // NT Lan Man authentication OSVERSIONINFO ver; ZeroObj(ver); ver.dwOSVersionInfoSize = sizeof(ver); if (!GetVersionEx(&ver)) { DWORD err = GetLastError(); Log("Couldn't get OS version", GSocketI::SocketMsgError); } else { // Username is in the format: User[@Domain] char UserDom[256]; strcpy_s(UserDom, sizeof(UserDom), User); char *Domain = strchr(UserDom, '@'); if (Domain) *Domain++ = 0; int AuthCmd = d->NextCmd++; sprintf_s(Buf, sizeof(Buf), "A%04.4i AUTHENTICATE NTLM\r\n", AuthCmd); if (WriteBuf()) { if (ReadResponse(AuthCmd, true)) { tSmbNtlmAuthNegotiate negotiate; tSmbNtlmAuthChallenge challenge; tSmbNtlmAuthResponse response; buildSmbNtlmAuthNegotiate(&negotiate, 0, 0); if (NTLM_VER(&negotiate) == 2) { negotiate.v2.version.major = (uint8) ver.dwMajorVersion; negotiate.v2.version.minor = (uint8) ver.dwMinorVersion; negotiate.v2.version.buildNumber = (uint16) ver.dwBuildNumber; negotiate.v2.version.ntlmRevisionCurrent = 0x0f; } ZeroObj(Buf); int negotiateLen = SmbLength(&negotiate); int c = ConvertBinaryToBase64(Buf, sizeof(Buf), (uchar*)&negotiate, negotiateLen); strcpy_s(Buf+c, sizeof(Buf)-c, "\r\n"); WriteBuf(false, NULL, true); /* read challange data from server, convert from base64 */ Buf[0] = 0; ClearDialog(); if (ReadResponse()) { /* buffer should contain the string "+ [base 64 data]" */ #if 1 ZeroObj(challenge); char *Line = Dialog.First(); LgiAssert(Line != NULL); ChopNewLine(Line); int LineLen = strlen(Line); int challengeLen = sizeof(challenge); c = ConvertBase64ToBinary((uchar*) &challenge, sizeof(challenge), Line+2, LineLen-2); if (NTLM_VER(&challenge) == 2) challenge.v2.bufIndex = c - (challenge.v2.buffer-(uint8*)&challenge); else challenge.v1.bufIndex = c - (challenge.v1.buffer-(uint8*)&challenge); #endif /* prepare response, convert to base64, send to server */ ZeroObj(response); FILETIME time = {0, 0}; SYSTEMTIME stNow; GetSystemTime(&stNow); SystemTimeToFileTime(&stNow, &time); char HostName[256] = ""; gethostname(HostName, sizeof(HostName)); buildSmbNtlmAuthResponse(&challenge, &response, UserDom, HostName, Domain, Password, (uint8*)&time); if (NTLM_VER(&response) == 2) { response.v2.version.major = (uint8) ver.dwMajorVersion; response.v2.version.minor = (uint8) ver.dwMinorVersion; response.v2.version.buildNumber = (uint16) ver.dwBuildNumber; response.v2.version.ntlmRevisionCurrent = 0x0f; } #if 0 { uint8 *r1 = (uint8*)&response; uint8 *r2 = (uint8*)&response_good; for (int i=0; iNextCmd++; sprintf_s(Buf, sizeof(Buf), "A%4.4i AUTHENTICATE DIGEST-MD5\r\n", AuthCmd); if (WriteBuf()) { if (ReadResponse(AuthCmd)) { char *TestCnonce = 0; #if 0 // Test case strcpy(Buf, "+ cmVhbG09ImVsd29vZC5pbm5vc29mdC5jb20iLG5vbmNlPSJPQTZNRzl0RVFHbTJoaCIscW9wPSJhdXRoIixhbGdvcml0aG09bWQ1LXNlc3MsY2hhcnNldD11dGYtOA=="); RemoteHost = "elwood.innosoft.com"; User = "chris"; Password = "secret"; TestCnonce = "OA6MHXh6VqTrRk"; #endif char *In = (char*)Buf; if (In[0] == '+' && In[1] == ' ') { In += 2; uchar Out[2048]; ssize_t b = ConvertBase64ToBinary(Out, sizeof(Out), In, strlen(In)); Out[b] = 0; LHashTbl, char*> Map; char *s = (char*)Out; while (s && *s) { char *Var = Tok(s); char *Eq = Tok(s); char *Val = Tok(s); char *Comma = Tok(s); if (Var && Eq && Val && strcmp(Eq, "=") == 0) { Map.Add(Var, Val); Val = 0; } DeleteArray(Var); DeleteArray(Eq); DeleteArray(Val); DeleteArray(Comma); } int32 CnonceI[2] = { (int32)LgiRand(), (int32)LgiRand() }; char Cnonce[32]; if (TestCnonce) strcpy_s(Cnonce, sizeof(Cnonce), TestCnonce); else Cnonce[ConvertBinaryToBase64(Cnonce, sizeof(Cnonce), (uchar*)&CnonceI, sizeof(CnonceI))] = 0; s = strchr(Cnonce, '='); if (s) *s = 0; int Nc = 1; char *Realm = Map.Find("realm"); char DigestUri[256]; sprintf_s(DigestUri, sizeof(DigestUri), "imap/%s", Realm ? Realm : RemoteHost); GStringPipe p; p.Print("username=\"%s\"", User); p.Print(",nc=%08.8i", Nc); p.Print(",digest-uri=\"%s\"", DigestUri); p.Print(",cnonce=\"%s\"", Cnonce); char *Nonce = Map.Find("nonce"); if (Nonce) { p.Print(",nonce=\"%s\"", Nonce); } if (Realm) { p.Print(",realm=\"%s\"", Realm); } char *Charset = Map.Find("charset"); if (Charset) { p.Print(",charset=%s", Charset); } char *Qop = Map.Find("qop"); if (Qop) { p.Print(",qop=%s", Qop); } // Calculate A1 char a1[256]; uchar md5[16]; sprintf_s(Buf, sizeof(Buf), "%s:%s:%s", User, Realm ? Realm : (char*)"", Password); MDStringToDigest((uchar*)a1, Buf); char *Authzid = Map.Find("authzid"); int ch = 16; if (Authzid) ch += sprintf_s(a1+ch, sizeof(a1)-ch, ":%s:%s:%s", Nonce, Cnonce, Authzid); else ch += sprintf_s(a1+ch, sizeof(a1)-ch, ":%s:%s", Nonce, Cnonce); MDStringToDigest(md5, a1, ch); char a1hex[256]; Hex(a1hex, sizeof(a1hex), (uchar*)md5, sizeof(md5)); // Calculate char a2[256]; if (Qop && (_stricmp(Qop, "auth-int") == 0 || _stricmp(Qop, "auth-conf") == 0)) sprintf_s(a2, sizeof(a2), "AUTHENTICATE:%s:00000000000000000000000000000000", DigestUri); else sprintf_s(a2, sizeof(a2), "AUTHENTICATE:%s", DigestUri); MDStringToDigest(md5, a2); char a2hex[256]; Hex(a2hex, sizeof(a2hex), (uchar*)md5, sizeof(md5)); // Calculate the final response sprintf_s(Buf, sizeof(Buf), "%s:%s:%8.8i:%s:%s:%s", a1hex, Nonce, Nc, Cnonce, Qop, a2hex); MDStringToDigest(md5, Buf); Hex(Buf, sizeof(Buf), (uchar*)md5, sizeof(md5)); p.Print(",response=%s", Buf); if ((s = p.NewStr())) { ssize_t Chars = ConvertBinaryToBase64(Buf, sizeof(Buf) - 4, (uchar*)s, strlen(s)); LgiAssert(Chars < sizeof(Buf)); strcpy_s(Buf+Chars, sizeof(Buf)-Chars, "\r\n"); if (WriteBuf(false, NULL, true) && Read()) { for (char *Dlg = Dialog.First(); Dlg; Dlg=Dialog.Next()) { if (Dlg[0] == '+' && Dlg[1] == ' ') { Log(Dlg, GSocketI::SocketMsgReceive); strcpy_s(Buf, sizeof(Buf), "\r\n"); if (WriteBuf(false, NULL, true)) { LoggedIn = ReadResponse(AuthCmd); } } else { Log(Dlg, GSocketI::SocketMsgError); break; } } } DeleteArray(s); } } } CommandFinished(); } } else if (!_stricmp(AuthType, "XOAUTH2")) { if (stristr(RemoteHost, "office365.com")) { Log("office365.com doesn't support OAUTH2:", GSocketI::SocketMsgInfo); Log("\thttps://stackoverflow.com/questions/29747477/imap-auth-in-office-365-using-oauth2", GSocketI::SocketMsgInfo); Log("\tSo why does it report support in the CAPABILITY response? Don't ask me - fret", GSocketI::SocketMsgInfo); continue; } - else if (!d->OAuth.IsValid()) + else if (!OAuth2.IsValid()) { sprintf_s(Buf, sizeof(Buf), "Error: Unknown OAUTH2 server '%s' (ask fret@memecode.com to add)", RemoteHost); Log(Buf, GSocketI::SocketMsgError); continue; } - GString Uri; - GString RedirUri; - GString AuthCode; - GString SessionState; - #if DEBUG_OAUTH2 - LgiTrace("%s:%i - SettingStore=%p\n", _FL, SettingStore); - #endif - if (SettingStore) - { - GVariant v; - if (SettingStore->GetValue(OPT_ImapOAuth2AccessToken, v)) - d->OAuth.AccessToken = v.Str(); - } - - #if DEBUG_OAUTH2 - LgiTrace("%s:%i - AccessToken=%s\n", _FL, d->OAuth.AccessToken.Get()); - #endif - if (!d->OAuth.AccessToken) - { - OAuthWebServer WebServer(55220); - - // Launch browser to get an authorization code - bool UsingLocalhost = WebServer.GetPort() > 0; - #if DEBUG_OAUTH2 - LgiTrace("%s:%i - UsingLocalhost=%i\n", _FL, UsingLocalhost); - #endif - if (UsingLocalhost) - { - // In this case the local host webserver is successfully listening - // on a port and ready to receive the redirect. - RedirUri.Printf("http://localhost:%i", WebServer.GetPort()); - #if DEBUG_OAUTH2 - LgiTrace("%s:%i - RedirUri=%s\n", _FL, RedirUri.Get()); - #endif - } - else - { - // Something went wrong with the localhost web server and we need to - // provide an alternative way of getting the AuthCode - GString::Array a = d->OAuth.RedirURIs.Split("\n"); - for (unsigned i=0; iOAuth.AuthUri.Get(), - d->OAuth.ClientID.Get(), - RedirEnc.Get(), - d->OAuth.Scope.Get()); - #if DEBUG_OAUTH2 - bool ExResult; - LgiTrace("%s:%i - Uri=%p-%p\n", _FL, Uri.Get(), Uri.Get() + Uri.Length()); - ExResult = - #endif - LgiExecute(Uri); - #if DEBUG_OAUTH2 - LgiTrace("%s:%i - LgiExecute(%s)=%i\n", _FL, Uri.Get(), ExResult); - #endif - - if (UsingLocalhost) - { - // Wait for localhost web server to receive the response - LCancel LocalCancel; - GString Req = WebServer.GetRequest(d->Cancel ? d->Cancel : &LocalCancel); - if (Req) - { - LHashTbl,GString> Map; - GString::Array a = Req.Split("\r\n"); - if (a.Length() > 0) - { - GString::Array p = a[0].Split(" "); - if (p.Length() > 1) - { - ssize_t Q = p[1].Find("?"); - if (Q >= 0) - { - GString Params = p[1](Q+1, -1); - a = Params.Split("&"); - for (unsigned i=0; i\n" - "\n" - "\n" - "\n" - "OAuth2Client: %s\n" - "\n", - AuthCode.Get() ? "Received auth code OK" : "Failed to get auth code"); - - WebServer.SetResponse(Resp); - - // Wait for the response to get sent... - uint64 Start = LgiCurrentTime(); - while (!WebServer.IsFinished()) - { - if (LgiCurrentTime() - Start > 5000) - break; - LgiSleep(50); - } - } - } - else - { - #ifdef WINDOWS - // Allow the user to paste the Auth Token in. - GInput Dlg(d->ParentWnd, "", "Enter Authorization Token:", "IMAP OAuth2 Authentication"); - if (Dlg.DoModal()) - { - AuthCode = Dlg.GetStr(); - #if DEBUG_OAUTH2 - LgiTrace("%s:%i - AuthCode=%s\n", _FL, AuthCode.Get()); - #endif - } - #else - LgiTrace("%s:%i - No fallback for UI token.\n", _FL); - break; - #endif - } - - GStringPipe OutputLog; - if (ValidStr(AuthCode) && - ValidStr(RedirUri)) - { - // Now exchange the Auth Token for an Access Token (OMG this is so complicated). - Uri = d->OAuth.ApiUri; - GUri u(Uri); - - IHttp Http; - GStringPipe In, Out; - - In.Print("code="); - StrFormEncode(In, AuthCode, true); - - In.Print("&redirect_uri="); - StrFormEncode(In, RedirUri, true); - - In.Print("&client_id="); - StrFormEncode(In, d->OAuth.ClientID, true); - - // In.Print("&scope="); - - In.Print("&client_secret="); - StrFormEncode(In, d->OAuth.ClientSecret, true); - - In.Print("&grant_type=authorization_code"); - - if (d->OAuth.Proxy.Host) - { - Http.SetProxy( d->OAuth.Proxy.Host, - d->OAuth.Proxy.Port ? d->OAuth.Proxy.Port : HTTP_PORT); - #if DEBUG_OAUTH2 - LgiTrace("%s:%i - d->OAuth.Proxy=%s:%i\n", _FL, - d->OAuth.Proxy.Host, - d->OAuth.Proxy.Port); - #endif - } - - SslSocket *ssl; - GAutoPtr Ssl(ssl = new SslSocket(&OutputLog)); - if (Ssl) - { - ssl->SetSslOnConnect(true); - Ssl->SetTimeout(10 * 1000); - if (Http.Open( Ssl, - u.Host, - u.Port ? u.Port : HTTPS_PORT)) - { - int StatusCode = 0; - int ContentLength = (int)In.GetSize(); - char Hdrs[256]; - sprintf_s(Hdrs, sizeof(Hdrs), - "Content-Type: application/x-www-form-urlencoded\r\n" - "Content-Length: %i\r\n", - ContentLength); - bool Result = Http.Post(Uri, &In, &StatusCode, &Out, NULL, Hdrs); - GString sOut = Out.NewGStr(); - LJson Json; - - if (Result && Json.SetJson(sOut)) - { - d->OAuth.AccessToken = Json.Get("access_token"); - if (d->OAuth.AccessToken) - { - d->OAuth.RefreshToken = Json.Get("refresh_token"); - d->OAuth.ExpiresIn = (int)Json.Get("expires_in").Int(); - #if DEBUG_OAUTH2 - LgiTrace("%s:%i - OAuth:\n\tAccessToken=%s\n\tRefreshToken=%s\n\tExpires=%i\n", - _FL, - d->OAuth.AccessToken.Get(), - d->OAuth.RefreshToken.Get(), - d->OAuth.ExpiresIn); - #endif - } - else - { - GString Err = Json.Get("error"); - if (Err) - { - GString Description = Json.Get("error_description"); - #if DEBUG_OAUTH2 - LgiTrace("%s:%i - Error: %s (%s)\n", - _FL, - Err.Get(), - Description.Get()); - #endif - sprintf_s(Buf, sizeof(Buf), "Error: %s / %s", Err.Get(), Description.Get()); - Log(Buf, GSocketI::SocketMsgWarning); - } - } - } - else - { - #if DEBUG_OAUTH2 - LgiTrace("%s:%i - Error getting or parsing JSON:\n%s\n", _FL, sOut.Get()); - #endif - Log("Failed to parse JSON.", GSocketI::SocketMsgError); - } - } - else - { - Log(Http.GetErrorString(), GSocketI::SocketMsgError); - } - } - } - } - - - // Bail if there is no access token - if (!ValidStr(d->OAuth.AccessToken)) + TraceLog TLog; + LOAuth2 Auth(OAuth2, User, SettingStore, &TLog); + auto AccessToken = Auth.GetAccessToken(); + if (!AccessToken) { sprintf_s(Buf, sizeof(Buf), "Warning: No OAUTH2 Access Token."); #if DEBUG_OAUTH2 LgiTrace("%s:%i - %s.\n", _FL, Buf); #endif Log(Buf, GSocketI::SocketMsgWarning); break; } - + // Construct the XOAUTH2 parameter GString s; - s.Printf("user=%s\001auth=Bearer %s\001\001", User, d->OAuth.AccessToken.Get()); + s.Printf("user=%s\001auth=Bearer %s\001\001", User, AccessToken.Get()); #if DEBUG_OAUTH2 LgiTrace("%s:%i - s=%s.\n", _FL, s.Replace("\001", "%01").Get()); #endif Base64Str(s); - + // Issue the IMAP command int AuthCmd = d->NextCmd++; GString AuthStr; AuthStr.Printf("A%4.4i AUTHENTICATE XOAUTH2 %s\r\n", AuthCmd, s.Get()); if (WriteBuf(false, AuthStr)) { Dialog.DeleteArrays(); if (Read(NULL)) { for (char *l = Dialog.First(); l; l = Dialog.Next()) { if (*l == '+') { l++; while (*l && strchr(WhiteSpace, *l)) l++; s = l; UnBase64Str(s); Log(s, GSocketI::SocketMsgError); - + LJson t; t.SetJson(s); int StatusCode = (int)t.Get("status").Int(); LgiTrace("%s:%i - HTTP status: %i\n%s\n", _FL, StatusCode, s.Get()); sprintf_s(Buf, sizeof(Buf), "\r\n"); WriteBuf(false, NULL, true); - + if (StatusCode == 400) { // Refresh the token...? } } else if (*l == '*') { Log(l, GSocketI::SocketMsgReceive); } else { if (IsResponse(l, AuthCmd, LoggedIn) && LoggedIn) { Log(l, GSocketI::SocketMsgReceive); - + if (SettingStore) { // Login successful, so persist the AuthCode for next time - GVariant v = d->OAuth.AccessToken.Get(); + GVariant v = AccessToken.Get(); bool b = SettingStore->SetValue(OPT_ImapOAuth2AccessToken, v); if (!b) { Log("Couldn't store access token.", GSocketI::SocketMsgWarning); } } break; } else { Log(l, GSocketI::SocketMsgError); } } } } CommandFinished(); } - + if (!LoggedIn && SettingStore) { GVariant v; SettingStore->SetValue(OPT_ImapOAuth2AccessToken, v); break; } + } else { char s[256]; sprintf_s(s, sizeof(s), "Warning: Unsupported authentication type '%s'", AuthType); Log(s, GSocketI::SocketMsgWarning); } } if (LoggedIn) { Status = true; // Ask server for it's heirarchy (folder) separator. int Cmd = d->NextCmd++; sprintf_s(Buf, sizeof(Buf), "A%4.4i LIST \"\" \"\"\r\n", Cmd); if (WriteBuf()) { ClearDialog(); Buf[0] = 0; if (ReadResponse(Cmd)) { for (char *Dlg = Dialog.First(); Dlg; Dlg=Dialog.Next()) { GArray t; char *s = Dlg; while (*s) { GAutoString a = ImapBasicTokenize(s); if (a) t.New() = a; else break; } if (t.Length() >= 5 && strcmp(t[0], "*") == 0 && _stricmp(t[1], "list") == 0) { for (unsigned i=2; iFolderSep = *s; break; } } break; } } } CommandFinished(); } } else { SetError(L_ERROR_UNSUPPORTED_AUTH, "Authentication failed, types available:\n\t%s", ValidStr(AuthTypeStr) ? AuthTypeStr : "(none)"); } } } Unlock(); } return Status; } bool MailIMap::Close() { bool Status = false; if (Socket && Lock(_FL)) { if (d->ExpungeOnExit) { ExpungeFolder(); } int Cmd = d->NextCmd++; sprintf_s(Buf, sizeof(Buf), "A%4.4i LOGOUT\r\n", Cmd); if (WriteBuf()) { Status = true; } CommandFinished(); Unlock(); } return Status; } bool MailIMap::GetCapabilities(GArray &s) { // char *k = 0; // for (bool p=d->Capability.First(&k); p; p=d->Capability.Next(&k)) for (auto i : d->Capability) { s.Add(i.key); } return s.Length() > 0; } bool MailIMap::ServerOption(char *Opt) { return d->Capability.Find(Opt) != 0; } char *MailIMap::GetSelectedFolder() { return d->Current; } bool MailIMap::SelectFolder(const char *Path, StrMap *Values) { bool Status = false; if (Socket && Lock(_FL)) { int Cmd = d->NextCmd++; char *Enc = EncodePath(Path); sprintf_s(Buf, sizeof(Buf), "A%4.4i SELECT \"%s\"\r\n", Cmd, Enc); DeleteArray(Enc); if (WriteBuf()) { DeleteArray(d->Current); ClearDialog(); if (ReadResponse(Cmd)) { Uid.DeleteArrays(); if (Values) { for (GString Dlg = Dialog.First(); Dlg; Dlg = Dialog.Next()) { GString::Array t = Dlg.SplitDelimit(" []"); if (t.Length() > 0 && t[0].Equals("*")) { for (unsigned i=1; iAdd(t[i], t[i-1]); } else if (t[i].Equals("unseen")) { //char *val = t[i+1]; if (t[i+1].IsNumeric()) Values->Add(t[i], t[i+1]); } else if (t[i].Equals("flags")) { ssize_t s = Dlg.Find("("); ssize_t e = Dlg.Find(")", s + 1); if (e >= 0) { GString Val = Dlg(s+1, e); Values->Add(t[i], Val); } } } } } } Status = true; d->Current = NewStr(Path); ClearDialog(); } CommandFinished(); } Unlock(); } return Status; } int MailIMap::GetMessages(const char *Path) { int Status = 0; if (Socket && Lock(_FL)) { StrMap f; if (SelectFolder(Path, &f)) { GString Exists = f.Find("exists"); if (Exists && Exists.Int() >= 0) Status = (int)Exists.Int(); else LgiTrace("%s:%i - Failed to get 'exists' value.\n", _FL); } Unlock(); } return Status; } int MailIMap::GetMessages() { return GetMessages("INBOX"); } char *MailIMap::SequenceToString(GArray *Seq) { if (!Seq) return NewStr("1:*"); GStringPipe p; // int Last = 0; for (unsigned s=0; sLength(); ) { unsigned e = s; while (eLength()-1 && (*Seq)[e] == (*Seq)[e+1]-1) e++; if (s) p.Print(","); if (e == s) p.Print("%i", (*Seq)[s]); else p.Print("%i:%i", (*Seq)[s], (*Seq)[e]); s = e + 1; } return p.NewStr(); } static void RemoveBytes(GArray &a, ssize_t &Used, ssize_t Bytes) { if (Used >= Bytes) { ssize_t Remain = Used - Bytes; if (Remain > 0) memmove(&a[0], &a[Bytes], Remain); Used -= Bytes; } else LgiAssert(0); } static bool PopLine(GArray &a, ssize_t &Used, GAutoString &Line) { for (ssize_t i=0; iNextCmd++; GStringPipe p(256); p.Print("A%4.4i %sFETCH ", Cmd, ByUid ? "UID " : ""); p.Write(Seq, strlen(Seq)); p.Print(" (%s)\r\n", Parts); GAutoString WrBuf(p.NewStr()); if (WriteBuf(false, WrBuf)) { ClearDialog(); GArray Buf; Buf.Length(1024 + (SizeHint>0?(uint32)SizeHint:0)); ssize_t Used = 0; ssize_t MsgSize; // int64 Start = LgiCurrentTime(); int64 Bytes = 0; bool Done = false; // uint64 TotalTs = 0; bool Blocking = Socket->IsBlocking(); Socket->IsBlocking(false); #if DEBUG_FETCH LgiTrace("%s:%i - Fetch: Starting loop\n", _FL); #endif uint64 LastActivity = LgiCurrentTime(); bool Debug = false; while (!Done && Socket->IsOpen()) { ssize_t r; // We don't wait for 'readable' with select here because // a) The socket is in non-blocking mode and // b) For OpenSSL connections 'readable' on the socket != can get bytes from 'read'. // Just try the read first and see if it gives you bytes, if not then 'select' on the socket. while (true) { // Extend the buffer if getting used up if (Buf.Length()-Used <= 256) { Buf.Length(Buf.Length() + (64<<10)); #if DEBUG_FETCH LgiTrace("%s:%i - Fetch: Ext buf: %i\n", _FL, Buf.Length()); #endif } // Try and read bytes from server. r = Socket->Read(Buf.AddressOf(Used), Buf.Length()-Used-1); // -1 for NULL terminator #if DEBUG_FETCH LgiTrace("%s:%i - Fetch: r=%i, used=%i, buf=%i\n", _FL, r, Used, Buf.Length()); #endif if (r > 0) { if (RawCopy) RawCopy->Write(&Buf[Used], r); Used += r; Bytes += r; LastActivity = LgiCurrentTime(); } else { LgiSleep(1); // Don't eat the whole CPU... break; } if (Debug) LgiTrace("%s:%i - Recv=%i\n", _FL, r); } // See if we can parse out a single response GArray Ranges; LgiAssert(Used < Buf.Length()); Buf[Used] = 0; // NULL terminate before we parse while (true) { MsgSize = ParseImapResponse(&Buf[0], Used, Ranges, 2); #if DEBUG_FETCH LgiTrace("%s:%i - Fetch: MsgSize=%i\n", _FL, MsgSize); #endif if (Debug) LgiTrace("%s:%i - ParseImapResponse=%i\n", _FL, MsgSize); if (!MsgSize) break; if (!Debug) LastActivity = LgiCurrentTime(); char *b = &Buf[0]; if (MsgSize > Used) { // This is an error... ParseImapResponse should always return <= Used. // If this triggers, ParseImapResponse is skipping a NULL that it shouldn't. #if DEBUG_FETCH LgiTrace("%s:%i - Fetch: Wrong size %i, %i\n", _FL, MsgSize, Used); #endif Ranges.Length(0); LgiAssert(0); #if _DEBUG ParseImapResponse(&Buf[0], Used, Ranges, 2); #endif Done = true; break; } LgiAssert(Ranges.Length() >= 2); // Setup strings for callback char *Param = b + Ranges[0].Start; Param[Ranges[0].Len()] = 0; char *Name = b + Ranges[1].Start; Name[Ranges[1].Len()] = 0; if (_stricmp(Name, "FETCH")) { // Not the response we're looking for. #if DEBUG_FETCH LgiTrace("%s:%i - Fetch: Wrong response: %s\n", _FL, Name); #endif } else { // Process ranges into a hash table StrMap Parts; for (unsigned i=2; i 0 && Buf[0] != '*') { GAutoString Line; while (PopLine(Buf, Used, Line)) { #if DEBUG_FETCH LgiTrace("%s:%i - Fetch: Line='%s'\n", _FL, Line.Get()); #endif GToken t(Line, " \r\n"); if (t.Length() >= 2) { char *r = t[0]; if (*r == 'A') { bool IsOk = !_stricmp(t[1], "Ok"); int Response = atoi(r + 1); Log(Line, IsOk ? GSocketI::SocketMsgReceive : GSocketI::SocketMsgError); if (Response == Cmd) { Done = true; break; } } else Log(&Buf[0], GSocketI::SocketMsgError); } else { // This is normal behaviour... just don't have the end marker yet. Done = true; break; } } } } Socket->IsBlocking(Blocking); CommandFinished(); #if DEBUG_FETCH LgiTrace("%s:%i - Fetch finished, status=%i\n", _FL, Status); #endif } Unlock(); return Status; } bool IMapHeadersCallback(MailIMap *Imap, char *Msg, MailIMap::StrMap &Parts, void *UserData) { char *s = Parts.Find(sRfc822Header); if (s) { Parts.Delete(sRfc822Header); GAutoString *Hdrs = (GAutoString*)UserData; Hdrs->Reset(s); } return true; } char *MailIMap::GetHeaders(int Message) { GAutoString Text; if (Lock(_FL)) { char Seq[64]; sprintf_s(Seq, sizeof(Seq), "%i", Message + 1); Fetch( false, Seq, sRfc822Header, IMapHeadersCallback, &Text, NULL); Unlock(); } return Text.Release(); } struct ReceiveCallbackState { MailTransaction *Trans; MailCallbacks *Callbacks; }; static bool IMapReceiveCallback(MailIMap *Imap, char *Msg, MailIMap::StrMap &Parts, void *UserData) { ReceiveCallbackState *State = (ReceiveCallbackState*) UserData; char *Flags = Parts.Find("FLAGS"); if (Flags) { State->Trans->Imap.Set(Flags); } char *Hdrs = Parts.Find(sRfc822Header); if (Hdrs) { ssize_t Len = strlen(Hdrs); State->Trans->Stream->Write(Hdrs, Len); } char *Body = Parts.Find("BODY[TEXT]"); if (Body) { ssize_t Len = strlen(Body); State->Trans->Stream->Write(Body, Len); } State->Trans->Status = Hdrs != NULL || Body != NULL; if (Imap->Items) Imap->Items->Value++; Parts.Empty(); if (State->Callbacks) { bool Ret = State->Callbacks->OnReceive(State->Trans, State->Callbacks->CallbackData); if (!Ret) return false; } return true; } bool MailIMap::Receive(GArray &Trans, MailCallbacks *Callbacks) { bool Status = false; if (Lock(_FL)) { int Errors = 0; ReceiveCallbackState State; State.Callbacks = Callbacks; for (unsigned i=0; iStatus = false; char Seq[64]; sprintf_s(Seq, sizeof(Seq), "%i", State.Trans->Index + 1); Fetch ( false, Seq, "FLAGS RFC822.HEADER BODY[TEXT]", IMapReceiveCallback, &State, NULL ); if (State.Trans->Status) { Status = true; } else if (Errors++ > 5) { // Yeah... not feelin' it Status = false; break; } } Unlock(); } return Status; } bool MailIMap::Append(const char *Folder, ImapMailFlags *Flags, const char *Msg, GString &NewUid) { bool Status = false; if (Folder && Msg && Lock(_FL)) { GAutoString Flag(Flags ? Flags->Get() : NULL); GAutoString Path(EncodePath(Folder)); int Cmd = d->NextCmd++; int Len = 0; for (const char *m = Msg; *m; m++) { if (*m == '\n') { Len += 2; } else if (*m != '\r') { Len++; } } // Append on the end of the mailbox int c = sprintf_s(Buf, sizeof(Buf), "A%4.4i APPEND \"%s\"", Cmd, Path.Get()); if (Flag) c += sprintf_s(Buf+c, sizeof(Buf)-c, " (%s)", Flag.Get()); c += sprintf_s(Buf+c, sizeof(Buf)-c, " {%i}\r\n", Len); if (WriteBuf()) { if (Read()) { bool GotPlus = false; for (char *Dlg = Dialog.First(); Dlg; Dlg = Dialog.Next()) { if (Dlg[0] == '+') { Dialog.Delete(Dlg); DeleteArray(Dlg); GotPlus = true; break; } } if (GotPlus) { int Wrote = 0; for (const char *m = Msg; *m; ) { while (*m == '\r' || *m == '\n') { if (*m == '\n') { Wrote += Socket->Write((char*)"\r\n", 2); } m++; } const char *e = m; while (*e && *e != '\r' && *e != '\n') e++; if (e > m) { Wrote += Socket->Write(m, e-m); m = e; } else break; } LgiAssert(Wrote == Len); Wrote += Socket->Write((char*)"\r\n", 2); // Read response.. ClearDialog(); if ((Status = ReadResponse(Cmd))) { char Tmp[16]; sprintf_s(Tmp, sizeof(Tmp), "A%4.4i", Cmd); for (char *Line = Dialog.First(); Line; Line = Dialog.Next()) { GAutoString c = ImapBasicTokenize(Line); if (!c) break; if (!strcmp(Tmp, c)) { GAutoString a; while ((a = ImapBasicTokenize(Line)).Get()) { GToken t(a, " "); if (t.Length() > 2 && !_stricmp(t[0], "APPENDUID")) { NewUid = t[2]; break; } } } } } } } CommandFinished(); } Unlock(); } return Status; } bool MailIMap::Delete(int Message) { bool Status = false; if (Socket && Lock(_FL)) { int Cmd = d->NextCmd++; sprintf_s(Buf, sizeof(Buf), "A%4.4i STORE %i FLAGS (\\deleted)\r\n", Cmd, Message+1); if (WriteBuf()) { ClearDialog(); Status = ReadResponse(Cmd); CommandFinished(); } Unlock(); } return Status; } bool MailIMap::Delete(bool ByUid, const char *Seq) { bool Status = false; if (Socket && Lock(_FL)) { int Cmd = d->NextCmd++; sprintf_s(Buf, sizeof(Buf), "A%4.4i %sSTORE %s FLAGS (\\deleted)\r\n", Cmd, ByUid?"UID ":"", Seq); if (WriteBuf()) { ClearDialog(); Status = ReadResponse(Cmd); CommandFinished(); } Unlock(); } return Status; } int MailIMap::Sizeof(int Message) { int Status = 0; if (Socket && Lock(_FL)) { int Cmd = d->NextCmd++; sprintf_s(Buf, sizeof(Buf), "A%4.4i FETCH %i (%s)\r\n", Cmd, Message+1, sRfc822Size); if (WriteBuf()) { ClearDialog(); Buf[0] = 0; if (ReadResponse(Cmd)) { char *d = Dialog.First(); if (d) { char *t = strstr(d, sRfc822Size); if (t) { t += strlen(sRfc822Size) + 1; Status = atoi(t); } } } CommandFinished(); } Unlock(); } return Status; } bool ImapSizeCallback(MailIMap *Imap, char *Msg, MailIMap::StrMap &Parts, void *UserData) { GArray *Sizes = (GArray*) UserData; int Index = atoi(Msg); if (Index < 1) return false; char *Sz = Parts.Find(sRfc822Size); if (!Sz) return false; (*Sizes)[Index - 1] = atoi(Sz); return true; } bool MailIMap::GetSizes(GArray &Sizes) { return Fetch(false, "1:*", sRfc822Size, ImapSizeCallback, &Sizes) != 0; } bool MailIMap::GetUid(int Message, char *Id, int IdLen) { bool Status = false; if (Lock(_FL)) { if (FillUidList()) { char *s = Uid.ItemAt(Message); if (s && Id) { strcpy_s(Id, IdLen, s); Status = true; } } Unlock(); } return Status; } bool MailIMap::FillUidList() { bool Status = false; if (Socket && Lock(_FL)) { if (Uid.Length() == 0) { int Cmd = d->NextCmd++; sprintf_s(Buf, sizeof(Buf), "A%4.4i UID SEARCH ALL\r\n", Cmd); if (WriteBuf()) { ClearDialog(); if (ReadResponse(Cmd)) { for (char *d = Dialog.First(); d && !Status; d=Dialog.Next()) { GToken T(d, " "); if (T[1] && strcmp(T[1], "SEARCH") == 0) { for (unsigned i=2; i &Id) { bool Status = false; if (Lock(_FL)) { if (FillUidList()) { for (char *s=Uid.First(); s; s=Uid.Next()) { Id.Insert(NewStr(s)); } Status = true; } Unlock(); } return Status; } bool MailIMap::GetFolders(GArray &Folders) { bool Status = false; if (Socket && Lock(_FL)) { int Cmd = d->NextCmd++; sprintf_s(Buf, sizeof(Buf), "A%4.4i LIST \"\" \"*\"\r\n", Cmd); if (WriteBuf()) { ClearDialog(); Buf[0] = 0; if (ReadResponse(Cmd)) { char Sep[] = { GetFolderSep(), 0 }; for (char *d = Dialog.First(); d; d=Dialog.Next()) { GArray t; char *s; while ((s = LgiTokStr((const char*&)d))) { t[t.Length()].Reset(s); } if (t.Length() >= 5) { if (strcmp(t[0], "*") == 0 && _stricmp(t[1], "LIST") == 0) { char *Folder = t[t.Length()-1]; MailImapFolder *f = new MailImapFolder(); if (f) { Folders.Add(f); f->Sep = Sep[0]; // Check flags f->NoSelect = stristr(t[2], "NoSelect") != 0; f->NoInferiors = stristr(t[2], "NoInferiors") != 0; // LgiTrace("Imap folder '%s' %s\n", Folder, t[2].Get()); // Alloc name if (Folder[0] == '\"') { char *p = TrimStr(Folder, "\""); f->Path = DecodeImapString(p); DeleteArray(p); } else { f->Path = DecodeImapString(Folder); } } } } } Status = true; ClearDialog(); } CommandFinished(); } Unlock(); } return Status; } bool MailIMap::CreateFolder(MailImapFolder *f) { bool Status = false; // char Dir[2] = { d->FolderSep, 0 }; if (f && f->GetPath() && Lock(_FL)) { int Cmd = d->NextCmd++; char *Enc = EncodePath(f->GetPath()); sprintf_s(Buf, sizeof(Buf), "A%4.4i CREATE \"%s\"\r\n", Cmd, Enc); DeleteArray(Enc); if (WriteBuf()) { ClearDialog(); Status = ReadResponse(Cmd); if (Status) { char *End = f->Path + strlen(f->Path) - 1; if (*End == GetFolderSep()) { f->NoSelect = true; *End = 0; } else { f->NoInferiors = true; } } CommandFinished(); } Unlock(); } return Status; } char *MailIMap::EncodePath(const char *Path) { if (!Path) return 0; char Sep = GetFolderSep(); char Native[MAX_PATH], *o = Native, *e = Native + sizeof(Native) - 1; for (const char *i = *Path == '/' ? Path + 1 : Path; *i && o < e; i++) { if (*i == '/') *o++ = Sep; else *o++ = *i; } *o++ = 0; return EncodeImapString(Native); } bool MailIMap::DeleteFolder(const char *Path) { bool Status = false; if (Path && Lock(_FL)) { // Close the current folder if required. if (d->Current && _stricmp(Path, d->Current) == 0) { int Cmd = d->NextCmd++; sprintf_s(Buf, sizeof(Buf), "A%4.4i CLOSE\r\n", Cmd); if (WriteBuf()) { ClearDialog(); ReadResponse(Cmd); DeleteArray(d->Current); CommandFinished(); } } // Delete the folder int Cmd = d->NextCmd++; char *NativePath = EncodePath(Path); sprintf_s(Buf, sizeof(Buf), "A%4.4i DELETE \"%s\"\r\n", Cmd, NativePath); DeleteArray(NativePath); if (WriteBuf()) { ClearDialog(); Status = ReadResponse(Cmd); CommandFinished(); } Unlock(); } return Status; } bool MailIMap::RenameFolder(const char *From, const char *To) { bool Status = false; if (From && To && Lock(_FL)) { int Cmd = d->NextCmd++; GAutoString f(EncodePath(From)); GAutoString t(EncodePath(To)); sprintf_s(Buf, sizeof(Buf), "A%4.4i RENAME \"%s\" \"%s\"\r\n", Cmd, f.Get(), t.Get()); if (WriteBuf()) { ClearDialog(); Status = ReadResponse(Cmd); CommandFinished(); } Unlock(); } return Status; } bool MailIMap::SetFolderFlags(MailImapFolder *f) { bool Status = false; if (f && Lock(_FL)) { /* int Cmd = d->NextCmd++; sprintf_s(Buf, sizeof(Buf), "A%4.4i RENAME \"%s\" \"%s\"\r\n", Cmd); if (WriteBuf()) { ClearDialog(); Status = ReadResponse(Cmd); } */ Unlock(); } return Status; } bool MailIMap::SetFlagsByUid(GArray &Uids, const char *Flags) { bool Status = false; if (Lock(_FL)) { int Cmd = d->NextCmd++; GStringPipe p; p.Print("A%04.4i UID STORE ", Cmd); if (Uids.Length()) { for (unsigned i=0; i &InUids, const char *DestFolder) { bool Status = false; if (Lock(_FL)) { int Cmd = d->NextCmd++; GAutoString Dest(EncodePath(DestFolder)); GStringPipe p(1024); p.Print("A%04.4i UID COPY ", Cmd); for (unsigned i=0; iNextCmd++; sprintf_s(Buf, sizeof(Buf), "A%4.4i EXPUNGE\r\n", Cmd); if (WriteBuf()) { ClearDialog(); Status = ReadResponse(Cmd); CommandFinished(); } Unlock(); } return Status; } bool MailIMap::Search(bool Uids, GArray &SeqNumbers, const char *Filter) { bool Status = false; if (ValidStr(Filter) && Lock(_FL)) { int Cmd = d->NextCmd++; sprintf_s(Buf, sizeof(Buf), "A%4.4i %sSEARCH %s\r\n", Cmd, Uids?"UID ":"", Filter); if (WriteBuf()) { ClearDialog(); if (ReadResponse(Cmd)) { for (char *d = Dialog.First(); d; d = Dialog.Next()) { if (*d != '*') continue; d++; GAutoString s(Tok(d)); if (!s || _stricmp(s, "Search")) continue; while (s.Reset(Tok(d))) { SeqNumbers.New() = s; Status = true; } } } CommandFinished(); } Unlock(); } return Status; } bool MailIMap::Status(char *Path, int *Recent) { bool Status = false; if (Path && Recent && Lock(_FL)) { GAutoString Dest(EncodePath(Path)); int Cmd = d->NextCmd++; sprintf_s(Buf, sizeof(Buf), "A%4.4i STATUS %s (RECENT)\r\n", Cmd, Dest.Get()); if (WriteBuf()) { ClearDialog(); if (ReadResponse(Cmd)) { for (char *d=Dialog.First(); d; d=Dialog.Next()) { if (*d != '*') continue; d++; GAutoString Cmd = ImapBasicTokenize(d); GAutoString Folder = ImapBasicTokenize(d); GAutoString Fields = ImapBasicTokenize(d); if (Cmd && Folder && Fields && !_stricmp(Cmd, "status") && !_stricmp(Folder, Dest)) { char *f = Fields; GAutoString Field = ImapBasicTokenize(f); GAutoString Value = ImapBasicTokenize(f); if (Field && Value && !_stricmp(Field, "recent")) { *Recent = atoi(Value); Status = true; break; } } } } else LgiTrace("%s:%i - STATUS cmd failed.\n", _FL); CommandFinished(); } Unlock(); } return Status; } bool MailIMap::Poll(int *Recent, GArray *New) { bool Status = true; if (Lock(_FL)) { int Cmd = d->NextCmd++; sprintf_s(Buf, sizeof(Buf), "A%4.4i NOOP\r\n", Cmd); if (WriteBuf()) { ClearDialog(); if ((Status = ReadResponse(Cmd))) { int LocalRecent; if (!Recent) Recent = &LocalRecent; *Recent = 0; for (char *Dlg=Dialog.First(); Dlg; Dlg=Dialog.Next()) { if (Recent && stristr(Dlg, " RECENT")) { *Recent = atoi(Dlg + 2); } } if (*Recent && New) { Search(false, *New, "new"); } } CommandFinished(); } Unlock(); } return Status; } bool MailIMap::StartIdle() { bool Status = false; if (Lock(_FL)) { int Cmd = d->NextCmd++; sprintf_s(Buf, sizeof(Buf), "A%4.4i IDLE\r\n", Cmd); Status = WriteBuf(); CommandFinished(); Unlock(); } return Status; } bool MailIMap::OnIdle(int Timeout, GArray &Resp) { bool Status = false; if (Lock(_FL)) { auto Blk = Socket->IsBlocking(); Socket->IsBlocking(false); #if 0 // def _DEBUG auto Start = LgiCurrentTime(); #endif Read(NULL, Timeout); #if 0 // def _DEBUG auto Time = LgiCurrentTime() - Start; if (Timeout > 0 && Time < (uint64)(Timeout * 0.9)) { printf("Short rd " LPrintfInt64 " of %i\n", Time, Timeout); } #endif Socket->IsBlocking(Blk); char *Dlg; while ((Dlg = Dialog.First())) { Dialog.Delete(Dlg); Log(Dlg, GSocketI::SocketMsgReceive); if (Dlg[0] == '*' && Dlg[1] == ' ') { char *s = Dlg + 2; GAutoString a = ImapBasicTokenize(s); GAutoString b = ImapBasicTokenize(s); if (a && b) { Untagged &u = Resp.New(); if (IsDigit(a[0])) { u.Cmd = b.Get(); u.Id = atoi(a); if (ValidStr(s)) u.Param = s; } else { u.Param = Dlg + 2; } Status = true; } } DeleteArray(Dlg); } Unlock(); } return Status; } bool MailIMap::FinishIdle() { bool Status = false; if (Lock(_FL)) { if (WriteBuf(false, "DONE\r\n")) { Status = ReadResponse(); CommandFinished(); } Unlock(); } return Status; } diff --git a/src/common/Lgi/GSubProcess.cpp b/src/common/Lgi/GSubProcess.cpp --- a/src/common/Lgi/GSubProcess.cpp +++ b/src/common/Lgi/GSubProcess.cpp @@ -1,1023 +1,1024 @@ /** \file \brief Sub-process wrapper. This class runs one or more sub-processes chained together by pipes. Example: GSubProcess p1("ls", "-l"); GSubProcess p2("grep", "string"); p1.Connect(&p2); p1.Start(true, false); int r; char Buf[256]; while ((r = p1.Read(Buf, sizeof(Buf))) > 0) { // So something with 'Buf' } */ #if defined(MAC) || defined(POSIX) #define _GNU_SOURCE #include #include #include #include #endif #ifdef BEOS #include #endif #include "Lgi.h" #include "GSubProcess.h" #include "GToken.h" #define DEBUG_SUBPROCESS 0 #if defined(WIN32) #define NULL_PIPE NULL #define ClosePipe CloseHandle #else #define NULL_PIPE -1 #define ClosePipe close #define INVALID_PID -1 #endif GSubProcess::Pipe::Pipe() { Read = Write = NULL_PIPE; } bool GSubProcess::Pipe::Create ( #ifdef WIN32 LPSECURITY_ATTRIBUTES pAttr #else void *UnusedParam #endif ) { #if defined(WIN32) return CreatePipe(&Read, &Write, pAttr, 0) != 0; #else return pipe(Handles) != NULL_PIPE; #endif } void GSubProcess::Pipe::Close() { if (Read != NULL_PIPE) { ClosePipe(Read); Read = NULL_PIPE; } if (Write != NULL_PIPE) { ClosePipe(Write); Write = NULL_PIPE; } } GSubProcess::GSubProcess(const char *exe, const char *args) { #if defined(POSIX) ChildPid = INVALID_PID; ExitValue = -1; #elif defined(WIN32) ChildPid = NULL; ChildHnd = NULL; ExitValue = 0; #endif NewGroup = true; ErrorCode = 0; Parent = Child = NULL; Exe = exe; Args.Add(Exe); EnvironmentChanged = false; ExternIn = NULL_PIPE; ExternOut = NULL_PIPE; #if DEBUG_SUBPROCESS LgiTrace("%s:%i - %p::GSubProcess('%s','%s')\n", _FL, this, exe, args); #endif char *s; while ((s = LgiTokStr(args))) { Args.Add(s); } } GSubProcess::~GSubProcess() { #if defined(POSIX) Io.Close(); #endif if (Child) { LgiAssert(Child->Parent == this); Child->Parent = NULL; } if (Parent) { LgiAssert(Parent->Child == this); Parent->Child = NULL; } } #ifndef WINDOWS extern char **environ; #endif GSubProcess::Variable *GSubProcess::GetEnvVar(const char *Var, bool Create) { if (Environment.Length() == 0) { // Read all variables in #ifdef WINDOWS LPWCH e = GetEnvironmentStringsW(); if (e) { char16 *s = e; while (*s) { char16 *eq = StrchrW(s, '='); if (!eq) break; ptrdiff_t NameChars = eq - s; if (NameChars > 0) { Variable &v = Environment.New(); v.Var.SetW(s, eq - s); eq++; v.Val.SetW(eq); } eq += StrlenW(eq); s = eq + 1; } FreeEnvironmentStringsW(e); } #else for (int i=0; environ[i]; i++) { auto p = GString(environ[i]).Split("=", 1); if (p.Length() == 2) { Variable &v = Environment.New(); v.Var = p[0]; v.Val = p[1]; } } #endif } for (unsigned i=0; iVal.Get() : NULL; } bool GSubProcess::SetEnvironment(const char *Var, const char *Value) { Variable *v = GetEnvVar(Var, true); if (!v) return false; bool IsPath = !_stricmp(Var, "PATH"); GStringPipe a; const char *s = Value; while (*s) { char *n = strchr(s, '%'); char *e = n ? strchr(n + 1, '%') : NULL; if (n && e) { a.Write(s, (int) (n-s)); n++; ptrdiff_t bytes = e - n; char Name[128]; if (bytes > sizeof(Name) - 1) bytes = sizeof(Name)-1; memcpy(Name, n, bytes); Name[bytes] = 0; const char *existing = GetEnvironment(Name); if (existing) { a.Write(existing, (int)strlen(existing)); } s = e + 1; } else { a.Write(s, (int)strlen(s)); break; } } v->Val = a.NewGStr(); if (IsPath) { // Remove missing paths from the list GToken t(v->Val, LGI_PATH_SEPARATOR); GStringPipe p; for (unsigned i=0; iVal = p.NewGStr(); } EnvironmentChanged = true; return true; } bool GSubProcess::GetValue(const char *Var, ::GVariant &Value) { switch (LgiStringToDomProp(Var)) { case StreamReadable: { #ifdef WINNATIVE char Buf[32] = ""; DWORD lpBytesRead = 0; BOOL b = PeekNamedPipe( ChildOutput.Read, Buf, sizeof(Buf), &lpBytesRead, NULL, NULL); Value = b && lpBytesRead > 0; break; #endif } /* case StreamWritable: { break; } */ default: return false; } return true; } void GSubProcess::SetStdin(OsFile Hnd) { ExternIn = Hnd; } void GSubProcess::SetStdout(OsFile Hnd) { ExternOut = Hnd; } void GSubProcess::Connect(GSubProcess *child) { Child = child; if (Child) { Child->Parent = this; } } bool GSubProcess::Start(bool ReadAccess, bool WriteAccess, bool MapStderrToStdout) { bool Status = false; #if USE_SIMPLE_FORK int in[2]; if (pipe(in) == -1) { printf("parent: Failed to create stdin pipe"); return false; } int out[2]; if (pipe(out) == -1) { printf("parent: Failed to create stdout pipe"); return false; } ChildPid = fork(); if (ChildPid == 0) { // We are in the child process. if (InitialFolder) { chdir(InitialFolder); } // Child shouldn't write to its stdin. if (close(in[1])) printf("%s:%i - close failed.\n", _FL); // Child shouldn't read from its stdout. if (close(out[0])) printf("%s:%i - close failed.\n", _FL); // Redirect stdin and stdout for the child process. if (dup2(in[0], fileno(stdin)) == -1) { printf("%s:%i - child[pre-exec]: Failed to redirect stdin for child\n", _FL); return false; } if (close(in[0])) printf("%s:%i - close failed.\n", _FL); if (dup2(out[1], fileno(stdout)) == -1) { printf("%s:%i - child[pre-exec]: Failed to redirect stdout for child\n", _FL); return false; } if (dup2(out[1], fileno(stderr)) == -1) { printf("%s:%i - child[pre-exec]: Failed to redirect stderr for child\n", _FL); return false; } close(out[1]); // Execute the child Args.Add(NULL); if (Environment.Length()) { GString::Array Vars; GArray Env; Vars.SetFixedLength(false); for (auto v : Environment) { GString &s = Vars.New(); s.Printf("%s=%s", v.Var.Get(), v.Val.Get()); Env.Add(s.Get()); } Env.Add(NULL); execve(Exe, &Args[0], Env.AddressOf()); } else { execvp(Exe, &Args[0]); } // Execution will pass to here if the 'Exe' can't run or doesn't exist // So by exiting with an error the parent process can handle it. exit(GSUBPROCESS_ERROR); } else { // We are in the parent process. if (ChildPid == -1) { printf("%s:%i - parent: Failed to create child", _FL); return false; } // Parent shouldn't read from child's stdin. if (close(in[0])) printf("%s:%i - close failed.\n", _FL); // Parent shouldn't write to child's stdout. if (close(out[1])) printf("%s:%i - close failed.\n", _FL); Io.Read = out[0]; Io.Write = in[1]; // printf("USE_SIMPLE_FORK success.\n"); return true; } #else #if DEBUG_SUBPROCESS LgiTrace("%s:%i - %p::Start(%i,%i,%i)\n", _FL, this, ReadAccess, WriteAccess, MapStderrToStdout); #endif // Find the end of the process list ::GArray p; for (GSubProcess *s=this; s; s=s->Child) { LgiAssert(!s->Child || s->Child->Parent == s); p.Add(s); } size_t Kids = p.Length() + 1; #ifdef WIN32 SECURITY_ATTRIBUTES Attr; Attr.nLength = sizeof(SECURITY_ATTRIBUTES); Attr.bInheritHandle = true; Attr.lpSecurityDescriptor = NULL; #else int Attr = 0; #endif #if defined(POSIX) ::GArray Pipes; Pipes.Length(Kids); Pipes[0].Create(&Attr); #if DEBUG_SUBPROCESS LgiTrace("%s:%i - *PARENT* pipe[%i].create %i,%i\n", _FL, 0, Pipes[0].Read, Pipes[0].Write); #endif Status = true; for (int i=1; iChildPid = fork(); if (sp->ChildPid == INVALID_PID) { LgiTrace("%s:%i - fork failed with %i", _FL, errno); exit(1); } else if (sp->ChildPid == 0) { if (InitialFolder) { chdir(InitialFolder); } // Close irrelevant pipes for (int j = 0; j < i-1; j++) { #if DEBUG_SUBPROCESS LgiTrace("%s:%i - *CHILD* pipe[%i].close %i,%i\n", _FL, j, Pipes[j].Read, Pipes[j].Write); #endif Pipes[j].Close(); } // Set up STDIN and STDOUT Pipe &in = Pipes[i-1]; Pipe &out = Pipes[i]; #if DEBUG_SUBPROCESS LgiTrace("%s:%i - *CHILD* %i) Child init %i->'%s'->%i\n", _FL, i, in.Read, sp->Exe.Get(), out.Write); #endif #if DEBUG_SUBPROCESS LgiTrace("%s:%i - *CHILD* Dupe %i->%i\n", _FL, in.Read, STDIN_FILENO); #endif Dupe(in.Read, STDIN_FILENO); #if DEBUG_SUBPROCESS LgiTrace("%s:%i - *CHILD* Close %i\n", _FL, in.Write); #endif close(in.Write); #if DEBUG_SUBPROCESS LgiTrace("%s:%i - *CHILD* Dupe %i->%i\n", _FL, out.Write, STDOUT_FILENO); #endif Dupe(out.Write, STDOUT_FILENO); #if DEBUG_SUBPROCESS LgiTrace("%s:%i - *CHILD* Dupe %i->%i\n", out.Write, STDERR_FILENO); #endif Dupe(out.Write, STDERR_FILENO); #if DEBUG_SUBPROCESS LgiTrace("%s:%i - *CHILD* Close %i\n", _FL, out.Read); #endif close(out.Read); fsync(STDOUT_FILENO); LgiSleep(100); // Execute the child sp->Args.Add(NULL); execvp(sp->Exe, &sp->Args[0]); LgiTrace("%s:%i - execvp('%s').\n", _FL, sp->Exe.Get()); for (int i=0; iArgs.Length(); i++) LgiTrace("%s:%i - Args[%i]='%s'\n", _FL, i, sp->Args[i]); Status = false; break; } } // Close irrelevant pipes for (int j = 1; j < Kids - 1; j++) { #if DEBUG_SUBPROCESS LgiTrace("%s:%i - *PARENT* pipe[%i].close %i,%i\n", _FL, j, Pipes[j].Read, Pipes[j].Write); #endif Pipes[j].Close(); } #if DEBUG_SUBPROCESS LgiTrace("%s:%i - *PARENT* pipe[0].close %i, pipe[%i].close %i\n", _FL, Pipes[0].Read, Pipes.Length()-1, Pipes.Last().Write); #endif close(Pipes[0].Read); close(Pipes.Last().Write); // Set the input and output pipes for this sub-process. if (WriteAccess) Io.Write = Pipes[0].Write; else { #if DEBUG_SUBPROCESS LgiTrace("%s:%i - *PARENT* pipe[0].close %i\n", _FL, Pipes[0].Write); #endif close(Pipes[0].Write); } if (ReadAccess) Io.Read = Pipes.Last().Read; else { #if DEBUG_SUBPROCESS LgiTrace("%s:%i - *PARENT* pipe[%i].close %i\n", _FL, Pipes.Length()-1, Pipes.Last().Read); #endif close(Pipes.Last().Read); } // LgiTrace("Final Handles %i, %i\n", Io.Read, Io.Write); #elif defined(WIN32) GAutoWString WExe; if (FileExists(Exe)) { WExe.Reset(Utf8ToWide(Exe)); } else { char *Ext = LgiGetExtension(Exe); bool HasExt = Ext && _stricmp(Ext, "exe") == 0; #if defined(WIN32) && !defined(PLATFORM_MINGW) GToken p; char *sPath = NULL; size_t sSize; errno_t err = _dupenv_s(&sPath, &sSize, "PATH"); if (err == 0) p.Parse(sPath, LGI_PATH_SEPARATOR); free(sPath); #else GToken p(getenv("PATH"), LGI_PATH_SEPARATOR); #endif for (unsigned i=0; i 0) { WArg[Ch++] = ' '; } if (strchr(a, ' ')) Ch += swprintf_s(WArg+Ch, CountOf(WArg)-Ch, L"\"%s\"", aw.Get()); else Ch += swprintf_s(WArg+Ch, CountOf(WArg)-Ch, L"%s", aw.Get()); } #if DEBUG_SUBPROCESS LgiTrace("%s:%i - Args='%S'\n", _FL, WArg); #endif bool HasExternIn = ExternIn != NULL_PIPE; #if DEBUG_SUBPROCESS LgiTrace("%s:%i - Oringinal handles, out=%p, in=%p, HasExternIn=%i\n", _FL, OldStdout, OldStdin, HasExternIn); #endif if (ChildOutput.Create(&Attr) && (HasExternIn || ChildInput.Create(&Attr))) { if (!SetHandleInformation(ChildOutput.Read, HANDLE_FLAG_INHERIT, 0)) LgiTrace("%s:%i - SetHandleInformation failed.\n", _FL); if (!HasExternIn && !SetHandleInformation(ChildInput.Write, HANDLE_FLAG_INHERIT, 0)) LgiTrace("%s:%i - SetHandleInformation failed.\n", _FL); #if DEBUG_SUBPROCESS LgiTrace("%s:%i - Output Pipe: rd=%p, wr=%p\n", _FL, ChildOutput.Read, ChildOutput.Write); if (!HasExternIn) LgiTrace("%s:%i - Input Pipe: rd=%p, wr=%p\n", _FL, ChildInput.Read, ChildInput.Write); #endif STARTUPINFOW Info; ZeroObj(Info); Info.cb = sizeof(Info); PROCESS_INFORMATION ProcInfo; ZeroObj(ProcInfo); Info.dwFlags = STARTF_USESTDHANDLES; Info.hStdOutput = ChildOutput.Write; Info.hStdInput = HasExternIn ? ExternIn : ChildInput.Read; if (MapStderrToStdout) Info.hStdError = ChildOutput.Write; GAutoWString WInitialFolder(Utf8ToWide(InitialFolder)); #if DEBUG_SUBPROCESS LgiTrace("%s:%i - WInitialFolder=%S, EnvironmentChanged=%i\n", _FL, WInitialFolder.Get(), EnvironmentChanged); #endif GAutoWString WEnv; if (EnvironmentChanged) { GMemQueue q(256); for (unsigned i=0; i 0) p.Write(Buf, Rd); else break; } return p.NewGStr(); } ssize_t GSubProcess::Read(void *Buf, ssize_t Size, int TimeoutMs) { #if defined(POSIX) bool DoRead = true; if (TimeoutMs) { OsSocket s = Io.Read; if (ValidSocket(s)) { struct timeval t = {TimeoutMs / 1000, (TimeoutMs % 1000) * 1000}; fd_set r; FD_ZERO(&r); FD_SET(s, &r); int v = select((int)s+1, &r, 0, 0, &t); if (v > 0 && FD_ISSET(s, &r)) { DoRead = true; } else { // printf("SubProc not readable..\n"); return 0; } } else LgiTrace("%s:%i - Invalid socket.\n", _FL); } return (int)read(Io.Read, Buf, Size); #else DWORD Rd = -1, Sz; if (!ReadFile(ChildOutput.Read, Buf, AssertCast(Sz, Size), &Rd, NULL)) return -1; return Rd; #endif } int GSubProcess::Peek() { #if defined(POSIX) int bytesAvailable = 0; int r = ioctl(Io.Read, FIONREAD, &bytesAvailable); return r ? -1 : bytesAvailable; #else DWORD Rd = 0, Avail = 0; char Buf[32]; if (PeekNamedPipe(ChildOutput.Read, Buf, sizeof(Buf), &Rd, &Avail, NULL)) return Rd; return 0; #endif } bool GSubProcess::Write(GString s) { auto Wr = Write(s.Get(), s.Length()); return Wr == s.Length(); } ssize_t GSubProcess::Write(const void *Buf, ssize_t Size, int Flags) { #if defined(POSIX) return (int)write(Io.Write, Buf, Size); #else DWORD Wr = -1, Sz; if (!WriteFile(ChildInput.Write, Buf, AssertCast(Sz, Size), &Wr, NULL)) return -1; return Wr; #endif } diff --git a/src/common/Lgi/LgiMain.cpp b/src/common/Lgi/LgiMain.cpp --- a/src/common/Lgi/LgiMain.cpp +++ b/src/common/Lgi/LgiMain.cpp @@ -1,278 +1,278 @@ /** \file \author Matthew Allen */ #include "Lgi.h" #include "GToken.h" #ifdef LGI_SDL #include #endif #if defined(WINDOWS) && defined(_DEBUG) #include "crtdbg.h" #endif /** \brief The main entry point of a Lgi program To hide the differences between the different OS's standard entry points for programs the file LgiMain.cpp implements a simple platform specific entry point and then calls LgiMain() to pass execution onto your program. A simple LgiMain() looks like this: \code int LgiMain(OsAppArguments &Args) { GApp *App = new GApp("application/x-MyProgram", Args); if (App && App->IsOk()) { App->AppWnd = new MyWindow; App->Run(); delete App; } return 0; } \endcode */ extern int LgiMain ( /// The arguments passed in from the OS. OsAppArguments &AppArgs ); bool _BuildCheck() { #ifdef _DEBUG const char *AppBuild = "Debug"; #else const char *AppBuild = "Release"; #endif char *LgiBuild = (char*) (LgiIsReleaseBuild() ? "Release" : "Debug"); if (_stricmp(AppBuild, LgiBuild)) { LgiTrace("Build check failed, app=%s lgi=%s\n", AppBuild, LgiBuild); return LgiMsg( NULL, "This application and it's Dll's are mismatched:\n" "\n" " Application:\t%s\n" " Lgi:\t\t%s\n" "\n" "If you continue you may encounter crashes and other undesirable problems.\n" "Do you want to continue anyway?", "Warning", MB_YESNO, AppBuild, LgiBuild) == IDYES; } return true; } #ifdef WIN32 int #if _CONSOLE main(int args, char *arg[]) #else -WINAPI +WINAPI CALLBACK WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) #endif { #ifdef _DEBUG _CrtSetDbgFlag(_CRTDBG_CHECK_ALWAYS_DF); #endif int Status = 0; #if !_CONSOLE && WINNATIVE _lgi_app_instance = hInstance; #endif if (_BuildCheck()) { char16 *CL = (char16*)GetCommandLineW(); #if WINNATIVE OsAppArguments AppArgs; #if !_CONSOLE AppArgs.hInstance = hInstance; AppArgs.nCmdShow = nCmdShow; #endif if (*CL == '\"') { AppArgs.lpCmdLine = StrchrW(CL+1, '\"'); } else { AppArgs.lpCmdLine = StrchrW(CL+1, ' '); } if (AppArgs.lpCmdLine) AppArgs.lpCmdLine++; #else GArray Args; char16 *Ws = L" \t\r\n"; for (char16 *c = CL; *c; ) { while (*c && StrchrW(Ws, *c)) c++; if (*c == '\"' || *c == '\'') { char16 delim = *c++; char16 *end = StrchrW(c, delim); if (end) { Args.Add(WideToUtf8(c, end-c)); c = end + 1; } else { Args.Add(WideToUtf8(c)); break; } } else { char16 *end = c; while (*end && !StrchrW(Ws, *end)) end++; if (end > c) Args.Add(WideToUtf8(c, end-c)); c = end + (*end != 0); } } OsAppArguments AppArgs(Args.Length(), (const char**) &Args[0]); #endif Status = LgiMain(AppArgs); } return Status; } #else #if defined(MAC) && !COCOA pascal OSErr AppEventHandler(const AppleEvent *ae, AppleEvent *reply, SRefCon handlerRefcon) { OSErr err = eventNotHandledErr; LgiTrace("AppEventHandler called.\n"); if (ae->descriptorType == typeAppleEvent) { uint32 Code; Size Used = 0; DescType typeCode = 0; OSErr e = AEGetAttributePtr(ae, keyEventIDAttr, typeWildCard, &typeCode, &Code, sizeof(Code), &Used); if (e) { LgiTrace("%s:%i - AEGetAttributePtr failed (%i).\n", _FL, e); } else if (Code == kAEGetURL) { char urlbuf[512]; e = AEGetParamPtr(ae, keyDirectObject, typeUTF8Text, NULL, urlbuf, sizeof(urlbuf), &Used); if (e) LgiTrace("%s:%i - AEGetParamPtr failed (%i).\n", _FL, e); else if (Used < sizeof(urlbuf)) { urlbuf[Used] = 0; if (LgiApp && LgiApp->AppWnd) { LgiApp->AppWnd->PostEvent(M_URL, (GMessage::Param) new GString(urlbuf)); } else { LgiTrace("%s:%i - No AppWnd.\n", _FL); err = eventInternalErr; } } } else { LgiTrace("%s:%i - Support for '%4.4s' not implement.\n", _FL, &Code); err = eventParameterNotFoundErr; } } return err; } #endif int main(int Args, char **Arg) { int Status = 0; OsAppArguments AppArgs(Args, (const char**)Arg); #ifdef MAC #if 0 LgiTrace("Args=%i\n", Args); for (int i=0; i a; for (int i=0; i Spacers; GBox::Spacer *Dragging; GdcPt2 DragOffset; bool Dirty; GBoxPriv() { Dirty = false; Vertical = false; Dragging = NULL; } int GetBox(GRect &r) { return Vertical ? r.Y() : r.X(); } GBox::Spacer *HitTest(int x, int y) { for (int i=0; iUnregisterHook(this); DeleteObj(d); } bool GBox::IsVertical() { return d->Vertical; } void GBox::SetVertical(bool v) { if (d->Vertical != v) { d->Vertical = v; OnPosChange(); } } GBox::Spacer *GBox::GetSpacer(int idx) { if (Children.Length()) { while (d->Spacers.Length() < Children.Length() - 1) { Spacer &s = d->Spacers.New(); s.SizePx = DEFAULT_SPACER_PX; // s.Colour.c24(DEFAULT_SPACER_COLOUR24); } } return idx >= 0 && idx < d->Spacers.Length() ? &d->Spacers[idx] : NULL; } GViewI *GBox::GetViewAt(int i) { return Children[i]; } bool GBox::SetViewAt(uint32_t i, GViewI *v) { if (!v || i > Children.Length()) { return false; } if (v->GetParent()) v->Detach(); v->Visible(true); bool Status; if (i < Children.Length()) { // Remove existing view.. GViewI *existing = Children[i]; if (existing == v) return true; if (existing) existing->Detach(); Status = AddView(v, i); } else { Status = AddView(v); } if (Status) { AttachChildren(); } return Status; } void GBox::OnCreate() { AttachChildren(); OnPosChange(); GWindow *Wnd = GetWindow(); if (Wnd) Wnd->RegisterHook(this, GMouseEvents); } bool GBox::OnViewMouse(GView *v, GMouse &m) { // This hook allows the GBox to catch clicks nearby the splits even if the splits are too small // to grab normally. Consider the case of a split that is 1px wide. The active region needs to // be a little larger than that, however a normal click would go through to the child windows // on either side of the split rather than to the GBox. if (!m.IsMove() && m.Down()) { // Convert click to the local coordinates of this view GMouse Local = m; while (v && v != (GView*)this && v->GetParent()) { if (dynamic_cast(v)) return true; GRect p = v->GetPos(); Local.x += p.x1; Local.y += p.y1; GViewI *vi = v->GetParent(); v = vi ? vi->GetGView() : NULL; } if (v == (GView*)this) { // Is the click over our spacers? Spacer *s = d->HitTest(Local.x, Local.y); if (s) { // Pass the click to ourselves and prevent the normal view from getting it. OnMouseClick(Local); return false; } } } return true; } bool GBox::Pour(GRegion &r) { GRect *p = FindLargest(r); if (!p) return false; SetPos(*p); return true; } void GBox::OnPaint(GSurface *pDC) { if (d->Dirty) { d->Dirty = false; OnPosChange(); } GRect cli = GetClient(); GCssTools tools(GetCss(), GetFont()); cli = tools.PaintBorderAndPadding(pDC, cli); GColour cBack = StyleColour(GCss::PropBackgroundColor, GColour(LC_MED, 24)); size_t ChildViews = Children.Length(); if (ChildViews == 0) { pDC->Colour(cBack); pDC->Rectangle(&cli); } else { #if 0 // coverage check... pDC->Colour(GColour(255, 0, 255)); pDC->Rectangle(&cli); #endif GRegion Painted(cli); for (int i=0; iSpacers.Length(); i++) { Spacer &s = d->Spacers[i]; if (s.Colour.IsValid()) pDC->Colour(s.Colour); else pDC->Colour(cBack); pDC->Rectangle(&s.Pos); Painted.Subtract(&s.Pos); } for (auto c : Children) Painted.Subtract(&c->GetPos()); for (auto r = Painted.First(); r; r = Painted.Next()) { pDC->Colour(cBack); pDC->Rectangle(r); } } } struct BoxRange { int Min, Max; GCss::Len Size; GViewI *View; BoxRange() { Min = Max = 0; View = NULL; } }; void GBox::OnPosChange() { GCssTools tools(GetCss(), GetFont()); GRect client = GetClient(); if (!client.Valid()) return; GRect content = tools.ApplyBorder(client); content = tools.ApplyPadding(content); GetSpacer(0); GAutoPtr views(IterateViews()); int Cur = content.x1, Idx = 0; int AvailablePx = d->GetBox(content); if (AvailablePx <= 0) return; GArray Sizes; int SpacerPx = 0; int FixedPx = 0; int FixedChildren = 0; int PercentPx = 0; int PercentChildren = 0; float PercentCount = 0.0f; int AutoChildren = 0; // Do first pass over children and find their sizes for (GViewI *c = views->First(); c; c = views->Next(), Idx++) { GCss *css = c->GetCss(); BoxRange &box = Sizes.New(); box.View = c; // Get any available CSS size if (css) { if (IsVertical()) box.Size = css->Height(); else box.Size = css->Width(); } // Work out some min and max values if (box.Size.IsValid()) { if (box.Size.Type == GCss::LenPercent) { box.Max = box.Size.ToPx(AvailablePx, GetFont()); PercentPx += box.Max; PercentCount += box.Size.Value; PercentChildren++; } else if (box.Size.IsDynamic()) { AutoChildren++; } else { // Fixed children get first crack at the space box.Min = box.Max = box.Size.ToPx(AvailablePx, GetFont()); FixedPx += box.Min; FixedChildren++; } } else AutoChildren++; // Allocate area for spacers in the Fixed portion if (Idx < Children.Length() - 1) { Spacer &s = d->Spacers[Idx]; SpacerPx += s.SizePx; } } // Convert all the percentage sizes to px int RemainingPx = AvailablePx - SpacerPx - FixedPx; for (int i=0; i RemainingPx) { if (AutoChildren > 0 || PercentChildren > 1) { // Well... ah... we better leave _some_ space for them. int AutoPx = 16 * AutoChildren; float Ratio = ((float)RemainingPx - AutoPx) / PercentPx; int Px = (int) (box.Max * Ratio); box.Size.Type = GCss::LenPx; box.Size.Value = (float) Px; RemainingPx -= Px; } else { // We can just take all the space... box.Size.Type = GCss::LenPx; box.Size.Value = (float) RemainingPx; RemainingPx = 0; } } else { box.Size.Type = GCss::LenPx; box.Size.Value = (float) box.Max; RemainingPx -= box.Max; } } } // Convert auto children to px int AutoPx = AutoChildren > 0 ? RemainingPx / AutoChildren : 0; for (int i=0; i 1) { box.Size.Value = (float) AutoPx; RemainingPx -= AutoPx; } else { box.Size.Value = (float) RemainingPx; RemainingPx = 0; } AutoChildren--; } } for (int i=0; iVertical) { viewPos.y1 = Cur; viewPos.y2 = Cur + Px - 1; } else { viewPos.x1 = Cur; viewPos.x2 = Cur + Px - 1; } box.View->SetPos(viewPos); #ifdef WIN32 // This forces the update, otherwise the child's display lags till the // mouse is released *rolls eyes* box.View->Invalidate((GRect*)NULL, true); #endif Cur += Px; // Allocate area for spacer if (i < Sizes.Length() - 1) { Spacer &s = d->Spacers[i]; s.Pos = content; if (d->Vertical) { s.Pos.y1 = Cur; s.Pos.y2 = Cur + s.SizePx - 1; } else { s.Pos.x1 = Cur; s.Pos.x2 = Cur + s.SizePx - 1; } Cur += s.SizePx; } } } void GBox::OnMouseClick(GMouse &m) { if (m.Down()) { d->Dragging = d->HitTest(m.x, m.y); if (d->Dragging) { d->DragOffset.x = m.x - d->Dragging->Pos.x1; d->DragOffset.y = m.y - d->Dragging->Pos.y1; Capture(d->Dragging != NULL); } } else if (IsCapturing()) { Capture(false); d->Dragging = NULL; } } bool IsValidLen(GCss *c, GCss::PropType p) { if (!c || c->GetType(p) != GCss::TypeLen) return false; GCss::Len *l = (GCss::Len*)c->PropAddress(p); if (!l) return false; return l->IsValid(); } void GBox::OnMouseMove(GMouse &m) { if (!d->Dragging || !IsCapturing()) return; if (!m.Down()) { // Something else got the up click? Capture(false); return; } int DragIndex = (int) (d->Dragging - &d->Spacers[0]); if (DragIndex < 0 || DragIndex >= d->Spacers.Length()) { LgiAssert(0); return; } GViewI *Prev = Children[DragIndex]; if (!Prev) { LgiAssert(0); return; } GViewI *Next = DragIndex < Children.Length() ? Children[DragIndex+1] : NULL; GCssTools tools(GetCss(), GetFont()); GRect Content = tools.ApplyMargin(GetClient()); int ContentPx = d->GetBox(Content); GRect SplitPos = d->Dragging->Pos; GCss *PrevStyle = Prev->GetCss(); GCss::PropType Style = d->Vertical ? GCss::PropHeight : GCss::PropWidth; bool EditPrev = !Next || IsValidLen(PrevStyle, Style); GViewI *Edit = EditPrev ? Prev : Next; LgiAssert(Edit != NULL); GRect ViewPos = Edit->GetPos(); auto *EditCss = Edit->GetCss(true); if (d->Vertical) { // Work out the minimum height of the view GCss::Len MinHeight = EditCss->MinHeight(); int MinPx = MinHeight.IsValid() ? MinHeight.ToPx(ViewPos.Y(), Edit->GetFont()) : DEFAULT_MINIMUM_SIZE_PX; int Offset = m.y - d->DragOffset.y - SplitPos.y1; if (Offset) { // Slide up and down the Y axis // Limit to the min size GRect r = ViewPos; if (EditPrev) { r.y2 += Offset; if (r.Y() < MinPx) { int Diff = MinPx - r.Y(); Offset += Diff; r.y2 += Diff; } } else { r.y1 += Offset; if (r.Y() < MinPx) { int Diff = MinPx - r.Y(); Offset -= Diff; r.y1 -= Diff; } } if (Offset) { SplitPos.Offset(0, Offset); // Save the new height of the view GCss::Len Ht = EditCss->Height(); if (Ht.Type == GCss::LenPercent && ContentPx > 0) { Ht.Value = (float)r.Y() * 100 / ContentPx; } else { Ht.Type = GCss::LenPx; Ht.Value = (float)r.Y(); } EditCss->Height(Ht); } } } else { // Work out the minimum width of the view GCss::Len MinWidth = EditCss->MinWidth(); int MinPx = MinWidth.IsValid() ? MinWidth.ToPx(ViewPos.X(), Edit->GetFont()) : DEFAULT_MINIMUM_SIZE_PX; int Offset = m.x - d->DragOffset.x - SplitPos.x1; if (Offset) { // Slide along the X axis // Limit to the min size GRect r = ViewPos; if (EditPrev) { r.x2 += Offset; int rx = r.X(); if (r.X() < MinPx) { int Diff = MinPx - rx; Offset += Diff; r.x2 += Diff; } } else { r.x1 += Offset; int rx = r.X(); if (r.X() < MinPx) { int Diff = MinPx - rx; Offset -= Diff; r.x1 -= Diff; } } if (Offset) { SplitPos.Offset(Offset, 0); // Save the new height of the view GCss::Len Wid = EditCss->Width(); if (Wid.Type == GCss::LenPercent && ContentPx > 0) { Wid.Value = (float)r.X() * 100 / ContentPx; } else { Wid.Type = GCss::LenPx; Wid.Value = (float)r.X(); } EditCss->Width(Wid); } } } OnPosChange(); Invalidate((GRect*)NULL, true); } + +int GBox::OnNotify(GViewI *Ctrl, int Flags) +{ + if (Flags == GNotifyTableLayout_Refresh) + { + d->Dirty = true; + if (Handle()) + PostEvent(M_CHILDREN_CHANGED); + } + + return GView::OnNotify(Ctrl, Flags); +} void GBox::OnChildrenChanged(GViewI *Wnd, bool Attaching) { #if 0 LgiTrace("GBox(%s)::OnChildrenChanged(%s, %i)\n", Name(), Wnd ? Wnd->GetClass() : NULL, Attaching); for (int i=0; iGetClass(), Children[i]->Handle(), Children[i]->Visible()); #endif d->Dirty = true; if (Handle()) PostEvent(M_CHILDREN_CHANGED); } int64 GBox::Value() { GViewI *v = Children.First(); if (!v) return 0; GCss *css = v->GetCss(); if (!css) return 0; GCss::Len l = d->Vertical ? css->Height() : css->Width(); if (l.Type != GCss::LenPx) return 0; return (int64)l.Value; } void GBox::Value(int64 i) { GViewI *v = Children.First(); if (!v) return; GCss *css = v->GetCss(true); if (!css) return; if (d->Vertical) css->Height(GCss::Len(GCss::LenPx, (float)i)); else css->Width(GCss::Len(GCss::LenPx, (float)i)); OnPosChange(); } LgiCursor GBox::GetCursor(int x, int y) { Spacer *Over = d->HitTest(x, y); if (Over) return (d->Vertical) ? LCUR_SizeVer : LCUR_SizeHor; else return LCUR_Normal; } bool GBox::OnLayout(GViewLayoutInfo &Inf) { Inf.Width.Min = -1; Inf.Width.Max = -1; Inf.Height.Min = -1; Inf.Height.Max = -1; return true; } bool GBox::Serialize(GDom *Dom, const char *OptName, bool Write) { if (Write) { } else { } LgiAssert(0); return false; } bool GBox::SetSize(int ViewIndex, GCss::Len Size) { GViewI *v = Children[ViewIndex]; if (!v) return false; GCss *c = v->GetCss(true); if (!c) return false; c->Width(Size); return true; } GMessage::Result GBox::OnEvent(GMessage *Msg) { if (Msg->Msg() == M_CHILDREN_CHANGED) { if (d->Dirty) { d->Dirty = false; OnPosChange(); } } return GView::OnEvent(Msg); }