agi.email.go 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020
  1. package agi
  2. /*
  3. AGI Email Library
  4. Author: tobychui
  5. Gives AGI scripts a full mail client backed by mod/email: IMAP accounts
  6. (Gmail, Outlook / Hotmail / Microsoft 365, Yahoo, iCloud and any IMAP/SMTP
  7. server), folders, reading, searching, flags, moving, drafts, sending with
  8. undo / scheduling, address book, labels, snoozing and OAuth sign-in.
  9. Usage (from an AGI script):
  10. requirelib("email");
  11. var accounts = email.listAccounts();
  12. if (accounts.success) {
  13. var page = email.list(accounts.data[0].id, {folder: "INBOX", page: 0});
  14. }
  15. Every function returns an object. On success it is {success: true, data: …};
  16. on failure {success: false, error: "…"} plus, where it applies,
  17. authFailed (the stored password / sign-in was rejected), hint (what the
  18. user should do, e.g. "use an app password") and code (notfound, blocked,
  19. toolarge).
  20. Accounts belong to the calling ArozOS user and are never visible to other
  21. users. Paths (attachments to send, folders to save into, .eml files) are
  22. ArozOS virtual paths and are permission and quota checked.
  23. */
  24. import (
  25. "context"
  26. "crypto/rand"
  27. "encoding/hex"
  28. "encoding/json"
  29. "errors"
  30. "fmt"
  31. "io"
  32. "os"
  33. "path/filepath"
  34. "strings"
  35. "sync"
  36. "time"
  37. "github.com/robertkrimen/otto"
  38. "imuslab.com/arozos/mod/agi/static"
  39. "imuslab.com/arozos/mod/email"
  40. "imuslab.com/arozos/mod/filesystem"
  41. "imuslab.com/arozos/mod/filesystem/arozfs"
  42. "imuslab.com/arozos/mod/info/logger"
  43. user "imuslab.com/arozos/mod/user"
  44. )
  45. const (
  46. emailReadTimeout = 2 * time.Minute
  47. emailSendTimeout = 10 * time.Minute
  48. emailMaxEMLBytes = 100 * 1024 * 1024
  49. emailTempRoot = "tmp:/Mail"
  50. emailTempMaxAge = 24 * time.Hour
  51. )
  52. var (
  53. emailTempCleanMutex sync.Mutex
  54. emailTempCleaned = map[string]time.Time{}
  55. )
  56. func (g *Gateway) EmailLibRegister() {
  57. err := g.RegisterLib("email", g.injectEmailLibFunctions)
  58. if err != nil {
  59. logger.PrintAndLog("Agi", fmt.Sprint(err), nil)
  60. os.Exit(1)
  61. }
  62. }
  63. // emailComposeInput is the composer payload: a ComposeRequest plus files
  64. // from the user's file system.
  65. type emailComposeInput struct {
  66. email.ComposeRequest
  67. Files []struct {
  68. Path string `json:"path"`
  69. Name string `json:"name"`
  70. } `json:"files"`
  71. }
  72. func (g *Gateway) injectEmailLibFunctions(payload *static.AgiLibInjectionPayload) {
  73. vm := payload.VM
  74. u := payload.User
  75. manager := g.Option.EmailManager
  76. principal := email.Principal{Username: u.Username, Admin: u.IsAdmin()}
  77. requestContext := func(timeout time.Duration) (context.Context, context.CancelFunc) {
  78. parent := context.Background()
  79. if payload.Request != nil {
  80. parent = payload.Request.Context()
  81. }
  82. return context.WithTimeout(parent, timeout)
  83. }
  84. //Sending must finish even if the browser gives up waiting
  85. detachedContext := func(timeout time.Duration) (context.Context, context.CancelFunc) {
  86. return context.WithTimeout(context.Background(), timeout)
  87. }
  88. ok := func(data interface{}) otto.Value { return emailResponse(vm, data, nil) }
  89. fail := func(err error) otto.Value { return emailResponse(vm, nil, err) }
  90. result := func(data interface{}, err error) otto.Value { return emailResponse(vm, data, err) }
  91. argString := func(call otto.FunctionCall, index int) string {
  92. value := call.Argument(index)
  93. if value.IsUndefined() || value.IsNull() {
  94. return ""
  95. }
  96. text, _ := value.ToString()
  97. return text
  98. }
  99. argUint := func(call otto.FunctionCall, index int) uint32 {
  100. value, err := call.Argument(index).ToInteger()
  101. if err != nil || value < 0 || value > int64(^uint32(0)) {
  102. return 0
  103. }
  104. return uint32(value)
  105. }
  106. argBool := func(call otto.FunctionCall, index int) bool {
  107. value, _ := call.Argument(index).ToBoolean()
  108. return value
  109. }
  110. argJSON := func(call otto.FunctionCall, index int, target interface{}) error {
  111. raw := argString(call, index)
  112. if raw == "" || raw == "undefined" {
  113. return nil
  114. }
  115. return json.Unmarshal([]byte(raw), target)
  116. }
  117. //Every call needs the backend; scripts get a clear answer when it is off
  118. guard := func(fn func(call otto.FunctionCall) otto.Value) func(call otto.FunctionCall) otto.Value {
  119. return func(call otto.FunctionCall) otto.Value {
  120. if manager == nil {
  121. return fail(errors.New("mail support is not enabled on this system"))
  122. }
  123. return fn(call)
  124. }
  125. }
  126. set := func(name string, fn func(call otto.FunctionCall) otto.Value) {
  127. vm.Set("_email_"+name, guard(fn))
  128. }
  129. /*
  130. Providers, discovery and OAuth
  131. */
  132. set("providers", func(call otto.FunctionCall) otto.Value {
  133. return ok(email.Presets())
  134. })
  135. set("discover", func(call otto.FunctionCall) otto.Value {
  136. ctx, cancel := requestContext(30 * time.Second)
  137. defer cancel()
  138. return result(manager.Discover(ctx, principal, argString(call, 0)))
  139. })
  140. set("oauthproviders", func(call otto.FunctionCall) otto.Value {
  141. return ok(manager.OAuthProviders())
  142. })
  143. set("oauthstart", func(call otto.FunctionCall) otto.Value {
  144. ctx, cancel := requestContext(30 * time.Second)
  145. defer cancel()
  146. return result(manager.OAuthStart(ctx, principal, argString(call, 0), argString(call, 1), argString(call, 2)))
  147. })
  148. set("oauthcomplete", func(call otto.FunctionCall) otto.Value {
  149. ctx, cancel := requestContext(45 * time.Second)
  150. defer cancel()
  151. return result(manager.OAuthComplete(ctx, principal, argString(call, 0), argString(call, 1)))
  152. })
  153. set("oauthstatus", func(call otto.FunctionCall) otto.Value {
  154. return result(manager.OAuthStatusOf(principal, argString(call, 0)))
  155. })
  156. set("oauthcancel", func(call otto.FunctionCall) otto.Value {
  157. manager.OAuthCancel(principal, argString(call, 0))
  158. return ok(true)
  159. })
  160. /*
  161. Accounts
  162. */
  163. set("listaccounts", func(call otto.FunctionCall) otto.Value {
  164. return result(manager.ListAccounts(principal))
  165. })
  166. set("getaccount", func(call otto.FunctionCall) otto.Value {
  167. return result(manager.GetAccount(principal, argString(call, 0)))
  168. })
  169. set("testaccount", func(call otto.FunctionCall) otto.Value {
  170. input := email.AccountInput{}
  171. if err := argJSON(call, 0, &input); err != nil {
  172. return fail(err)
  173. }
  174. ctx, cancel := requestContext(emailReadTimeout)
  175. defer cancel()
  176. test := manager.TestAccount(ctx, principal, input)
  177. if test.Error != "" {
  178. return emailFailureWith(vm, errors.New(test.Error), map[string]interface{}{
  179. "test": test, "authFailed": test.AuthFailed, "hint": test.Hint,
  180. })
  181. }
  182. return ok(test)
  183. })
  184. set("addaccount", func(call otto.FunctionCall) otto.Value {
  185. input := email.AccountInput{}
  186. if err := argJSON(call, 0, &input); err != nil {
  187. return fail(err)
  188. }
  189. ctx, cancel := detachedContext(emailReadTimeout)
  190. defer cancel()
  191. info, test, err := manager.AddAccount(ctx, principal, input)
  192. if err != nil {
  193. return emailFailureWith(vm, err, map[string]interface{}{
  194. "test": test, "authFailed": test.AuthFailed || email.IsAuthError(err), "hint": test.Hint,
  195. })
  196. }
  197. return ok(map[string]interface{}{"account": info, "test": test})
  198. })
  199. set("updateaccount", func(call otto.FunctionCall) otto.Value {
  200. input := email.AccountInput{}
  201. if err := argJSON(call, 1, &input); err != nil {
  202. return fail(err)
  203. }
  204. ctx, cancel := detachedContext(emailReadTimeout)
  205. defer cancel()
  206. info, test, err := manager.UpdateAccount(ctx, principal, argString(call, 0), input)
  207. if err != nil {
  208. return emailFailureWith(vm, err, map[string]interface{}{
  209. "test": test, "authFailed": test.AuthFailed || email.IsAuthError(err), "hint": test.Hint,
  210. })
  211. }
  212. return ok(map[string]interface{}{"account": info, "test": test})
  213. })
  214. set("removeaccount", func(call otto.FunctionCall) otto.Value {
  215. return result(true, manager.RemoveAccount(principal, argString(call, 0)))
  216. })
  217. set("reorderaccounts", func(call otto.FunctionCall) otto.Value {
  218. ids := []string{}
  219. if err := argJSON(call, 0, &ids); err != nil {
  220. return fail(err)
  221. }
  222. return result(true, manager.ReorderAccounts(principal, ids))
  223. })
  224. /*
  225. Folders
  226. */
  227. set("folders", func(call otto.FunctionCall) otto.Value {
  228. ctx, cancel := requestContext(emailReadTimeout)
  229. defer cancel()
  230. return result(manager.Folders(ctx, principal, argString(call, 0), argBool(call, 1)))
  231. })
  232. set("createfolder", func(call otto.FunctionCall) otto.Value {
  233. ctx, cancel := requestContext(emailReadTimeout)
  234. defer cancel()
  235. return result(manager.CreateFolder(ctx, principal, argString(call, 0), argString(call, 1), argString(call, 2)))
  236. })
  237. set("renamefolder", func(call otto.FunctionCall) otto.Value {
  238. ctx, cancel := requestContext(emailReadTimeout)
  239. defer cancel()
  240. return result(manager.RenameFolder(ctx, principal, argString(call, 0), argString(call, 1), argString(call, 2)))
  241. })
  242. set("deletefolder", func(call otto.FunctionCall) otto.Value {
  243. ctx, cancel := requestContext(emailReadTimeout)
  244. defer cancel()
  245. return result(true, manager.DeleteFolder(ctx, principal, argString(call, 0), argString(call, 1)))
  246. })
  247. set("emptyfolder", func(call otto.FunctionCall) otto.Value {
  248. ctx, cancel := requestContext(emailReadTimeout)
  249. defer cancel()
  250. return result(manager.EmptyFolder(ctx, principal, argString(call, 0), argString(call, 1)))
  251. })
  252. set("markallread", func(call otto.FunctionCall) otto.Value {
  253. ctx, cancel := requestContext(emailReadTimeout)
  254. defer cancel()
  255. return result(manager.MarkAllRead(ctx, principal, argString(call, 0), argString(call, 1)))
  256. })
  257. /*
  258. Messages
  259. */
  260. set("list", func(call otto.FunctionCall) otto.Value {
  261. query := email.ListQuery{}
  262. if err := argJSON(call, 1, &query); err != nil {
  263. return fail(err)
  264. }
  265. ctx, cancel := requestContext(emailReadTimeout)
  266. defer cancel()
  267. return result(manager.ListMessages(ctx, principal, argString(call, 0), query))
  268. })
  269. set("unified", func(call otto.FunctionCall) otto.Value {
  270. query := email.ListQuery{}
  271. if err := argJSON(call, 1, &query); err != nil {
  272. return fail(err)
  273. }
  274. accountIDs := []string{}
  275. if err := argJSON(call, 2, &accountIDs); err != nil {
  276. return fail(err)
  277. }
  278. ctx, cancel := requestContext(emailReadTimeout)
  279. defer cancel()
  280. return result(manager.UnifiedList(ctx, principal, argString(call, 0), accountIDs, query))
  281. })
  282. set("get", func(call otto.FunctionCall) otto.Value {
  283. options := email.GetOptions{}
  284. if err := argJSON(call, 3, &options); err != nil {
  285. return fail(err)
  286. }
  287. ctx, cancel := requestContext(emailReadTimeout)
  288. defer cancel()
  289. return result(manager.GetMessage(ctx, principal, argString(call, 0), argString(call, 1), argUint(call, 2), options))
  290. })
  291. set("rawsource", func(call otto.FunctionCall) otto.Value {
  292. ctx, cancel := requestContext(emailReadTimeout)
  293. defer cancel()
  294. raw, err := manager.GetRaw(ctx, principal, argString(call, 0), argString(call, 1), argUint(call, 2))
  295. if err != nil {
  296. return fail(err)
  297. }
  298. const limit = 2 * 1024 * 1024
  299. source := raw.Data
  300. truncated := false
  301. if len(source) > limit {
  302. source = source[:limit]
  303. truncated = true
  304. }
  305. return ok(map[string]interface{}{
  306. "source": strings.ToValidUTF8(string(source), "�"),
  307. "truncated": truncated,
  308. "size": len(raw.Data),
  309. })
  310. })
  311. uidList := func(call otto.FunctionCall, index int) ([]uint32, error) {
  312. uids := []uint32{}
  313. if err := argJSON(call, index, &uids); err != nil {
  314. return nil, errors.New("invalid message list")
  315. }
  316. return uids, nil
  317. }
  318. set("setflag", func(call otto.FunctionCall) otto.Value {
  319. uids, err := uidList(call, 2)
  320. if err != nil {
  321. return fail(err)
  322. }
  323. ctx, cancel := requestContext(emailReadTimeout)
  324. defer cancel()
  325. return result(manager.SetFlag(ctx, principal, argString(call, 0), argString(call, 1), uids, argString(call, 3), argBool(call, 4)))
  326. })
  327. set("move", func(call otto.FunctionCall) otto.Value {
  328. uids, err := uidList(call, 2)
  329. if err != nil {
  330. return fail(err)
  331. }
  332. ctx, cancel := requestContext(emailReadTimeout)
  333. defer cancel()
  334. return result(manager.Move(ctx, principal, argString(call, 0), argString(call, 1), uids, argString(call, 3)))
  335. })
  336. set("copy", func(call otto.FunctionCall) otto.Value {
  337. uids, err := uidList(call, 2)
  338. if err != nil {
  339. return fail(err)
  340. }
  341. ctx, cancel := requestContext(emailReadTimeout)
  342. defer cancel()
  343. return result(manager.Copy(ctx, principal, argString(call, 0), argString(call, 1), uids, argString(call, 3)))
  344. })
  345. set("movetorole", func(call otto.FunctionCall) otto.Value {
  346. uids, err := uidList(call, 2)
  347. if err != nil {
  348. return fail(err)
  349. }
  350. role := argString(call, 3)
  351. if role != email.RoleArchive && role != email.RoleJunk && role != email.RoleInbox && role != email.RoleTrash {
  352. return fail(errors.New("unknown destination " + role))
  353. }
  354. ctx, cancel := requestContext(emailReadTimeout)
  355. defer cancel()
  356. return result(manager.MoveToRole(ctx, principal, argString(call, 0), argString(call, 1), uids, role))
  357. })
  358. set("remove", func(call otto.FunctionCall) otto.Value {
  359. uids, err := uidList(call, 2)
  360. if err != nil {
  361. return fail(err)
  362. }
  363. ctx, cancel := requestContext(emailReadTimeout)
  364. defer cancel()
  365. return result(manager.Delete(ctx, principal, argString(call, 0), argString(call, 1), uids, argBool(call, 3)))
  366. })
  367. set("locate", func(call otto.FunctionCall) otto.Value {
  368. ctx, cancel := requestContext(emailReadTimeout)
  369. defer cancel()
  370. return result(manager.LocateMessage(ctx, principal, argString(call, 0), argString(call, 1), argString(call, 2)))
  371. })
  372. set("checkinboxes", func(call otto.FunctionCall) otto.Value {
  373. ctx, cancel := requestContext(emailReadTimeout)
  374. defer cancel()
  375. return result(manager.CheckInboxes(ctx, principal))
  376. })
  377. set("newsince", func(call otto.FunctionCall) otto.Value {
  378. ctx, cancel := requestContext(emailReadTimeout)
  379. defer cancel()
  380. limit, _ := call.Argument(2).ToInteger()
  381. return result(manager.NewSince(ctx, principal, argString(call, 0), argUint(call, 1), int(limit)))
  382. })
  383. /*
  384. Files in the ArozOS file system
  385. */
  386. set("savemessage", func(call otto.FunctionCall) otto.Value {
  387. ctx, cancel := requestContext(emailReadTimeout)
  388. defer cancel()
  389. raw, err := manager.GetRaw(ctx, principal, argString(call, 0), argString(call, 1), argUint(call, 2))
  390. if err != nil {
  391. return fail(err)
  392. }
  393. saved, err := emailWriteUserFile(u, payload.ScriptFsh, vm, argString(call, 3), raw.Filename, raw.Data)
  394. return result(map[string]interface{}{"path": saved}, err)
  395. })
  396. set("saveattachment", func(call otto.FunctionCall) otto.Value {
  397. ctx, cancel := requestContext(emailReadTimeout)
  398. defer cancel()
  399. part, err := manager.GetPart(ctx, principal, argString(call, 0), argString(call, 1), argUint(call, 2), argString(call, 3))
  400. if err != nil {
  401. return fail(err)
  402. }
  403. saved, err := emailWriteUserFile(u, payload.ScriptFsh, vm, argString(call, 4), part.Filename, part.Data)
  404. return result(map[string]interface{}{"path": saved, "filename": part.Filename, "contentType": part.ContentType}, err)
  405. })
  406. set("saveallattachments", func(call otto.FunctionCall) otto.Value {
  407. ids := []string{}
  408. if err := argJSON(call, 3, &ids); err != nil {
  409. return fail(err)
  410. }
  411. ctx, cancel := requestContext(emailSendTimeout)
  412. defer cancel()
  413. saved := []string{}
  414. for _, id := range ids {
  415. part, err := manager.GetPart(ctx, principal, argString(call, 0), argString(call, 1), argUint(call, 2), id)
  416. if err != nil {
  417. return emailFailureWith(vm, err, map[string]interface{}{"saved": saved})
  418. }
  419. path, err := emailWriteUserFile(u, payload.ScriptFsh, vm, argString(call, 4), part.Filename, part.Data)
  420. if err != nil {
  421. return emailFailureWith(vm, err, map[string]interface{}{"saved": saved})
  422. }
  423. saved = append(saved, path)
  424. }
  425. return ok(map[string]interface{}{"paths": saved})
  426. })
  427. set("openeml", func(call otto.FunctionCall) otto.Value {
  428. vpath := argString(call, 0)
  429. raw, err := emailReadUserFile(u, payload.ScriptFsh, vm, vpath, emailMaxEMLBytes)
  430. if err != nil {
  431. return fail(err)
  432. }
  433. message, err := manager.ParseEML(principal, raw, argBool(call, 1))
  434. if err != nil {
  435. return fail(err)
  436. }
  437. return ok(map[string]interface{}{"message": message, "path": vpath})
  438. })
  439. set("saveemlattachment", func(call otto.FunctionCall) otto.Value {
  440. raw, err := emailReadUserFile(u, payload.ScriptFsh, vm, argString(call, 0), emailMaxEMLBytes)
  441. if err != nil {
  442. return fail(err)
  443. }
  444. part, err := email.EMLPart(raw, argString(call, 1))
  445. if err != nil {
  446. return fail(err)
  447. }
  448. saved, err := emailWriteUserFile(u, payload.ScriptFsh, vm, argString(call, 2), part.Filename, part.Data)
  449. return result(map[string]interface{}{"path": saved, "filename": part.Filename, "contentType": part.ContentType}, err)
  450. })
  451. set("importeml", func(call otto.FunctionCall) otto.Value {
  452. raw, err := emailReadUserFile(u, payload.ScriptFsh, vm, argString(call, 0), emailMaxEMLBytes)
  453. if err != nil {
  454. return fail(err)
  455. }
  456. ctx, cancel := detachedContext(emailSendTimeout)
  457. defer cancel()
  458. uid, err := manager.ImportMessage(ctx, principal, argString(call, 1), argString(call, 2), raw)
  459. return result(map[string]interface{}{"uid": uid}, err)
  460. })
  461. set("tempfolder", func(call otto.FunctionCall) otto.Value {
  462. //A private scratch folder for uploads and downloads, pruned daily
  463. emailCleanTemp(u)
  464. purpose := strings.Trim(argString(call, 0), "/\\. ")
  465. if purpose != "uploads" && purpose != "downloads" {
  466. purpose = "downloads"
  467. }
  468. folder := emailTempRoot + "/" + purpose + "/" + time.Now().Format("20060102150405") + "-" + randomHex(4)
  469. fsh, rpath, err := static.VirtualPathToRealPath(folder, u)
  470. if err != nil {
  471. return fail(err)
  472. }
  473. if err := fsh.FileSystemAbstraction.MkdirAll(rpath, 0775); err != nil {
  474. return fail(err)
  475. }
  476. return ok(folder)
  477. })
  478. /*
  479. Composing
  480. */
  481. composeRequest := func(call otto.FunctionCall) (*email.ComposeRequest, error) {
  482. input := emailComposeInput{}
  483. if err := argJSON(call, 0, &input); err != nil {
  484. return nil, errors.New("invalid message: " + err.Error())
  485. }
  486. request := input.ComposeRequest
  487. for _, file := range input.Files {
  488. attachment, err := emailAttachmentFromPath(u, payload.ScriptFsh, vm, file.Path, file.Name)
  489. if err != nil {
  490. return nil, err
  491. }
  492. request.Attachments = append(request.Attachments, *attachment)
  493. }
  494. return &request, nil
  495. }
  496. set("send", func(call otto.FunctionCall) otto.Value {
  497. request, err := composeRequest(call)
  498. if err != nil {
  499. return fail(err)
  500. }
  501. ctx, cancel := detachedContext(emailSendTimeout)
  502. defer cancel()
  503. return result(manager.Send(ctx, principal, request))
  504. })
  505. set("savedraft", func(call otto.FunctionCall) otto.Value {
  506. request, err := composeRequest(call)
  507. if err != nil {
  508. return fail(err)
  509. }
  510. ctx, cancel := detachedContext(emailSendTimeout)
  511. defer cancel()
  512. return result(manager.SaveDraft(ctx, principal, request))
  513. })
  514. set("deletedraft", func(call otto.FunctionCall) otto.Value {
  515. ctx, cancel := requestContext(emailReadTimeout)
  516. defer cancel()
  517. return result(true, manager.DeleteDraft(ctx, principal, argString(call, 0), argString(call, 1), argUint(call, 2)))
  518. })
  519. set("outbox", func(call otto.FunctionCall) otto.Value {
  520. return result(manager.Outbox(principal))
  521. })
  522. set("outboxcancel", func(call otto.FunctionCall) otto.Value {
  523. ctx, cancel := detachedContext(emailReadTimeout)
  524. defer cancel()
  525. return result(manager.OutboxCancel(ctx, principal, argString(call, 0), argBool(call, 1)))
  526. })
  527. set("outboxsendnow", func(call otto.FunctionCall) otto.Value {
  528. return result(true, manager.OutboxSendNow(principal, argString(call, 0)))
  529. })
  530. /*
  531. Address book
  532. */
  533. set("contacts", func(call otto.FunctionCall) otto.Value {
  534. return result(manager.Contacts(principal))
  535. })
  536. set("searchcontacts", func(call otto.FunctionCall) otto.Value {
  537. limit, _ := call.Argument(1).ToInteger()
  538. return result(manager.SearchContacts(principal, argString(call, 0), int(limit)))
  539. })
  540. set("savecontact", func(call otto.FunctionCall) otto.Value {
  541. contact := email.Contact{}
  542. if err := argJSON(call, 0, &contact); err != nil {
  543. return fail(err)
  544. }
  545. return result(manager.SaveContact(principal, contact))
  546. })
  547. set("deletecontact", func(call otto.FunctionCall) otto.Value {
  548. return result(true, manager.DeleteContact(principal, argString(call, 0)))
  549. })
  550. set("importcontacts", func(call otto.FunctionCall) otto.Value {
  551. contacts := []email.Contact{}
  552. if err := argJSON(call, 0, &contacts); err != nil {
  553. return fail(err)
  554. }
  555. return result(manager.ImportContacts(principal, contacts))
  556. })
  557. /*
  558. Labels and snoozing
  559. */
  560. set("labels", func(call otto.FunctionCall) otto.Value {
  561. return ok(manager.Labels(principal))
  562. })
  563. set("savelabels", func(call otto.FunctionCall) otto.Value {
  564. labels := []email.Label{}
  565. if err := argJSON(call, 0, &labels); err != nil {
  566. return fail(err)
  567. }
  568. return result(manager.SaveLabels(principal, labels))
  569. })
  570. set("setlabels", func(call otto.FunctionCall) otto.Value {
  571. summary := email.MessageSummary{}
  572. if err := argJSON(call, 0, &summary); err != nil {
  573. return fail(err)
  574. }
  575. labels := []string{}
  576. if err := argJSON(call, 1, &labels); err != nil {
  577. return fail(err)
  578. }
  579. return result(true, manager.SetMessageLabels(principal, summary, labels))
  580. })
  581. set("labelmessages", func(call otto.FunctionCall) otto.Value {
  582. return result(manager.LabelMessages(principal, argString(call, 0)))
  583. })
  584. set("snooze", func(call otto.FunctionCall) otto.Value {
  585. summary := email.MessageSummary{}
  586. if err := argJSON(call, 0, &summary); err != nil {
  587. return fail(err)
  588. }
  589. until, _ := call.Argument(1).ToInteger()
  590. return result(true, manager.Snooze(principal, summary, until))
  591. })
  592. set("unsnooze", func(call otto.FunctionCall) otto.Value {
  593. summary := email.MessageSummary{}
  594. if err := argJSON(call, 0, &summary); err != nil {
  595. return fail(err)
  596. }
  597. return result(true, manager.Unsnooze(principal, summary))
  598. })
  599. set("snoozed", func(call otto.FunctionCall) otto.Value {
  600. return result(manager.SnoozedMessages(principal))
  601. })
  602. /*
  603. Preferences and administration
  604. */
  605. set("settings", func(call otto.FunctionCall) otto.Value {
  606. return ok(manager.Settings(principal))
  607. })
  608. set("savesettings", func(call otto.FunctionCall) otto.Value {
  609. settings := manager.Settings(principal)
  610. if err := argJSON(call, 0, &settings); err != nil {
  611. return fail(err)
  612. }
  613. return result(manager.SaveSettings(principal, settings))
  614. })
  615. set("trustsender", func(call otto.FunctionCall) otto.Value {
  616. return result(true, manager.TrustSender(principal, argString(call, 0)))
  617. })
  618. set("isadmin", func(call otto.FunctionCall) otto.Value {
  619. return ok(principal.Admin)
  620. })
  621. set("adminconfig", func(call otto.FunctionCall) otto.Value {
  622. if !principal.Admin {
  623. return fail(errors.New("permission denied"))
  624. }
  625. return ok(manager.AdminConfig())
  626. })
  627. set("setadminconfig", func(call otto.FunctionCall) otto.Value {
  628. input := email.AdminConfigInput{}
  629. if err := argJSON(call, 0, &input); err != nil {
  630. return fail(err)
  631. }
  632. return result(true, manager.SetAdminConfig(principal, input))
  633. })
  634. vm.Run(emailLibJavaScript)
  635. }
  636. // emailLibJavaScript wraps the native calls into the `email` object. Objects
  637. // cross the boundary as JSON so scripts always get plain JavaScript values.
  638. const emailLibJavaScript = `
  639. var email = {};
  640. (function(){
  641. var parse = function(raw) {
  642. if (raw === undefined || raw === null || raw === false) {
  643. return {success: false, error: "email call failed"};
  644. }
  645. try { return JSON.parse(raw); } catch (e) { return {success: false, error: "malformed email response"}; }
  646. };
  647. var json = function(value) {
  648. if (value === undefined || value === null) { return ""; }
  649. return JSON.stringify(value);
  650. };
  651. email.providers = function() { return parse(_email_providers()); };
  652. email.discover = function(address) { return parse(_email_discover(address)); };
  653. email.oauthProviders = function() { return parse(_email_oauthproviders()); };
  654. email.oauthStart = function(provider, address, redirectURI) { return parse(_email_oauthstart(provider, address || "", redirectURI || "")); };
  655. email.oauthComplete = function(state, codeOrURL) { return parse(_email_oauthcomplete(state || "", codeOrURL || "")); };
  656. email.oauthStatus = function(state) { return parse(_email_oauthstatus(state)); };
  657. email.oauthCancel = function(state) { return parse(_email_oauthcancel(state)); };
  658. email.listAccounts = function() { return parse(_email_listaccounts()); };
  659. email.getAccount = function(id) { return parse(_email_getaccount(id)); };
  660. email.testAccount = function(input) { return parse(_email_testaccount(json(input))); };
  661. email.addAccount = function(input) { return parse(_email_addaccount(json(input))); };
  662. email.updateAccount = function(id, input) { return parse(_email_updateaccount(id, json(input))); };
  663. email.removeAccount = function(id) { return parse(_email_removeaccount(id)); };
  664. email.reorderAccounts = function(ids) { return parse(_email_reorderaccounts(json(ids || []))); };
  665. email.folders = function(accountId, refresh) { return parse(_email_folders(accountId, refresh === true)); };
  666. email.createFolder = function(accountId, parent, name) { return parse(_email_createfolder(accountId, parent || "", name)); };
  667. email.renameFolder = function(accountId, folder, newName) { return parse(_email_renamefolder(accountId, folder, newName)); };
  668. email.deleteFolder = function(accountId, folder) { return parse(_email_deletefolder(accountId, folder)); };
  669. email.emptyFolder = function(accountId, folder) { return parse(_email_emptyfolder(accountId, folder)); };
  670. email.markAllRead = function(accountId, folder) { return parse(_email_markallread(accountId, folder)); };
  671. email.list = function(accountId, query) { return parse(_email_list(accountId, json(query || {}))); };
  672. email.unified = function(view, query, accountIds) { return parse(_email_unified(view || "inbox", json(query || {}), json(accountIds || []))); };
  673. email.get = function(accountId, folder, uid, options) { return parse(_email_get(accountId, folder, uid, json(options || {}))); };
  674. email.rawSource = function(accountId, folder, uid) { return parse(_email_rawsource(accountId, folder, uid)); };
  675. email.setFlag = function(accountId, folder, uids, flag, value) { return parse(_email_setflag(accountId, folder, json(uids), flag, value !== false)); };
  676. email.move = function(accountId, folder, uids, destination) { return parse(_email_move(accountId, folder, json(uids), destination)); };
  677. email.copy = function(accountId, folder, uids, destination) { return parse(_email_copy(accountId, folder, json(uids), destination)); };
  678. email.moveToRole = function(accountId, folder, uids, role) { return parse(_email_movetorole(accountId, folder, json(uids), role)); };
  679. email.remove = function(accountId, folder, uids, permanent) { return parse(_email_remove(accountId, folder, json(uids), permanent === true)); };
  680. email.locate = function(accountId, messageId, hint) { return parse(_email_locate(accountId, messageId, hint || "")); };
  681. email.checkInboxes = function() { return parse(_email_checkinboxes()); };
  682. email.newSince = function(accountId, uidNext, limit) { return parse(_email_newsince(accountId, uidNext, limit || 5)); };
  683. email.saveMessage = function(accountId, folder, uid, destDir) { return parse(_email_savemessage(accountId, folder, uid, destDir)); };
  684. email.saveAttachment = function(accountId, folder, uid, partId, destDir) { return parse(_email_saveattachment(accountId, folder, uid, partId, destDir)); };
  685. email.saveAllAttachments = function(accountId, folder, uid, partIds, destDir) { return parse(_email_saveallattachments(accountId, folder, uid, json(partIds || []), destDir)); };
  686. email.openEml = function(vpath, allowRemote) { return parse(_email_openeml(vpath, allowRemote === true)); };
  687. email.saveEmlAttachment = function(vpath, partId, destDir) { return parse(_email_saveemlattachment(vpath, partId, destDir)); };
  688. email.importEml = function(vpath, accountId, folder) { return parse(_email_importeml(vpath, accountId, folder || "INBOX")); };
  689. email.tempFolder = function(purpose) { return parse(_email_tempfolder(purpose || "downloads")); };
  690. email.send = function(message) { return parse(_email_send(json(message))); };
  691. email.saveDraft = function(message) { return parse(_email_savedraft(json(message))); };
  692. email.deleteDraft = function(accountId, folder, uid) { return parse(_email_deletedraft(accountId, folder, uid)); };
  693. email.outbox = function() { return parse(_email_outbox()); };
  694. email.outboxCancel = function(id, toDrafts) { return parse(_email_outboxcancel(id, toDrafts === true)); };
  695. email.outboxSendNow = function(id) { return parse(_email_outboxsendnow(id)); };
  696. email.contacts = function() { return parse(_email_contacts()); };
  697. email.searchContacts = function(query, limit) { return parse(_email_searchcontacts(query || "", limit || 8)); };
  698. email.saveContact = function(contact) { return parse(_email_savecontact(json(contact))); };
  699. email.deleteContact = function(address) { return parse(_email_deletecontact(address)); };
  700. email.importContacts = function(contacts) { return parse(_email_importcontacts(json(contacts || []))); };
  701. email.labels = function() { return parse(_email_labels()); };
  702. email.saveLabels = function(labels) { return parse(_email_savelabels(json(labels || []))); };
  703. email.setLabels = function(message, labelIds) { return parse(_email_setlabels(json(message), json(labelIds || []))); };
  704. email.labelMessages = function(labelId) { return parse(_email_labelmessages(labelId)); };
  705. email.snooze = function(message, until) { return parse(_email_snooze(json(message), until)); };
  706. email.unsnooze = function(message) { return parse(_email_unsnooze(json(message))); };
  707. email.snoozed = function() { return parse(_email_snoozed()); };
  708. email.settings = function() { return parse(_email_settings()); };
  709. email.saveSettings = function(settings) { return parse(_email_savesettings(json(settings))); };
  710. email.trustSender = function(sender) { return parse(_email_trustsender(sender)); };
  711. email.isAdmin = function() { return parse(_email_isadmin()); };
  712. email.adminConfig = function() { return parse(_email_adminconfig()); };
  713. email.setAdminConfig = function(config) { return parse(_email_setadminconfig(json(config))); };
  714. })();
  715. `
  716. // emailResponse builds the {success, data} / {success, error, …} envelope.
  717. func emailResponse(vm *otto.Otto, data interface{}, err error) otto.Value {
  718. if err != nil {
  719. return emailFailureWith(vm, err, nil)
  720. }
  721. encoded, merr := json.Marshal(map[string]interface{}{"success": true, "data": data})
  722. if merr != nil {
  723. return emailFailureWith(vm, merr, nil)
  724. }
  725. value, _ := vm.ToValue(string(encoded))
  726. return value
  727. }
  728. // emailFailureWith reports an error, classifying it for the front-end.
  729. func emailFailureWith(vm *otto.Otto, err error, extra map[string]interface{}) otto.Value {
  730. payload := map[string]interface{}{"success": false, "error": err.Error()}
  731. if email.IsAuthError(err) {
  732. payload["authFailed"] = true
  733. if hint := email.AuthHint(err); hint != "" {
  734. payload["hint"] = hint
  735. }
  736. }
  737. switch {
  738. case errors.Is(err, email.ErrAccountNotFound), errors.Is(err, email.ErrFolderNotFound), errors.Is(err, email.ErrMessageNotFound):
  739. payload["code"] = "notfound"
  740. case errors.Is(err, email.ErrBlockedAddress), errors.Is(err, email.ErrInsecureBlocked):
  741. payload["code"] = "blocked"
  742. case errors.Is(err, email.ErrTooLarge):
  743. payload["code"] = "toolarge"
  744. case errors.Is(err, email.ErrOAuthDisabled):
  745. payload["code"] = "oauthdisabled"
  746. }
  747. for key, value := range extra {
  748. if key == "hint" {
  749. if text, ok := value.(string); !ok || text == "" {
  750. continue
  751. }
  752. }
  753. if key == "authFailed" {
  754. if flag, ok := value.(bool); !ok || !flag {
  755. continue
  756. }
  757. }
  758. payload[key] = value
  759. }
  760. encoded, _ := json.Marshal(payload)
  761. value, _ := vm.ToValue(string(encoded))
  762. return value
  763. }
  764. // emailResolveFile resolves a readable file path for the calling user.
  765. func emailResolveFile(u *user.User, scriptFsh *filesystem.FileSystemHandler, vm *otto.Otto, vpath string) (*filesystem.FileSystemHandler, string, string, error) {
  766. vpath = strings.TrimSpace(vpath)
  767. if vpath == "" {
  768. return nil, "", "", errors.New("no file selected")
  769. }
  770. vpath = static.RelativeVpathRewrite(scriptFsh, vpath, vm, u)
  771. if !u.CanRead(vpath) {
  772. return nil, "", "", errors.New("access denied: " + vpath)
  773. }
  774. fsh, rpath, err := static.VirtualPathToRealPath(vpath, u)
  775. if err != nil {
  776. return nil, "", "", err
  777. }
  778. if !fsh.FileSystemAbstraction.FileExists(rpath) || fsh.FileSystemAbstraction.IsDir(rpath) {
  779. return nil, "", "", errors.New("file not found: " + vpath)
  780. }
  781. return fsh, rpath, vpath, nil
  782. }
  783. // emailReadUserFile reads a whole file (an .eml) with a size cap.
  784. func emailReadUserFile(u *user.User, scriptFsh *filesystem.FileSystemHandler, vm *otto.Otto, vpath string, maxBytes int64) ([]byte, error) {
  785. fsh, rpath, _, err := emailResolveFile(u, scriptFsh, vm, vpath)
  786. if err != nil {
  787. return nil, err
  788. }
  789. if size := fsh.FileSystemAbstraction.GetFileSize(rpath); size > maxBytes {
  790. return nil, email.ErrTooLarge
  791. }
  792. return fsh.FileSystemAbstraction.ReadFile(rpath)
  793. }
  794. // emailAttachmentFromPath turns a file the user picked into an attachment
  795. // that is streamed when the message is rendered.
  796. func emailAttachmentFromPath(u *user.User, scriptFsh *filesystem.FileSystemHandler, vm *otto.Otto, vpath string, name string) (*email.ComposeAttachment, error) {
  797. fsh, rpath, resolved, err := emailResolveFile(u, scriptFsh, vm, vpath)
  798. if err != nil {
  799. return nil, err
  800. }
  801. if strings.TrimSpace(name) == "" {
  802. name = filepath.Base(arozfs.ToSlash(resolved))
  803. }
  804. abstraction := fsh.FileSystemAbstraction
  805. return &email.ComposeAttachment{
  806. Name: name,
  807. Size: abstraction.GetFileSize(rpath),
  808. Open: func() (io.ReadCloser, error) { return abstraction.ReadStream(rpath) },
  809. }, nil
  810. }
  811. // emailWriteUserFile saves data into a folder of the user's file system under
  812. // a name that does not overwrite anything, and charges it to their quota.
  813. func emailWriteUserFile(u *user.User, scriptFsh *filesystem.FileSystemHandler, vm *otto.Otto, vdir string, filename string, data []byte) (string, error) {
  814. vdir = strings.TrimSpace(vdir)
  815. if vdir == "" {
  816. return "", errors.New("choose a folder to save into")
  817. }
  818. vdir = static.RelativeVpathRewrite(scriptFsh, vdir, vm, u)
  819. vdir = strings.TrimSuffix(vdir, "/")
  820. if strings.HasSuffix(vdir, ":") {
  821. vdir += "/"
  822. }
  823. if !u.CanWrite(vdir) {
  824. return "", errors.New("access denied: " + vdir)
  825. }
  826. if !u.StorageQuota.HaveSpace(int64(len(data))) {
  827. return "", errors.New("storage quota exceeded")
  828. }
  829. fsh, rdir, err := static.VirtualPathToRealPath(vdir, u)
  830. if err != nil {
  831. return "", err
  832. }
  833. if fsh.ReadOnly {
  834. return "", errors.New(fsh.Name + " is read only")
  835. }
  836. abstraction := fsh.FileSystemAbstraction
  837. if !abstraction.FileExists(rdir) {
  838. if err := abstraction.MkdirAll(rdir, 0775); err != nil {
  839. return "", err
  840. }
  841. }
  842. filename = emailSafeName(filename)
  843. base, ext := filename, filepath.Ext(filename)
  844. if ext != "" {
  845. base = strings.TrimSuffix(filename, ext)
  846. }
  847. name := filename
  848. for index := 1; abstraction.FileExists(arozfs.ToSlash(filepath.Join(rdir, name))); index++ {
  849. if index > 999 {
  850. return "", errors.New("too many files with the same name")
  851. }
  852. name = fmt.Sprintf("%s (%d)%s", base, index, ext)
  853. }
  854. target := arozfs.ToSlash(filepath.Join(rdir, name))
  855. if err := abstraction.WriteFile(target, data, 0775); err != nil {
  856. return "", err
  857. }
  858. vpath := strings.TrimSuffix(vdir, "/") + "/" + name
  859. u.SetOwnerOfFile(fsh, vpath)
  860. return vpath, nil
  861. }
  862. // emailSafeName keeps a server supplied file name inside its folder.
  863. func emailSafeName(name string) string {
  864. name = strings.TrimSpace(strings.NewReplacer("/", "_", "\\", "_", "\x00", "").Replace(name))
  865. name = strings.Trim(name, ". ")
  866. if name == "" {
  867. return "attachment"
  868. }
  869. return name
  870. }
  871. // emailCleanTemp prunes old scratch files under tmp:/Mail, at most hourly.
  872. func emailCleanTemp(u *user.User) {
  873. emailTempCleanMutex.Lock()
  874. last, seen := emailTempCleaned[u.Username]
  875. if seen && time.Since(last) < time.Hour {
  876. emailTempCleanMutex.Unlock()
  877. return
  878. }
  879. emailTempCleaned[u.Username] = time.Now()
  880. emailTempCleanMutex.Unlock()
  881. fsh, rpath, err := static.VirtualPathToRealPath(emailTempRoot, u)
  882. if err != nil || !fsh.FileSystemAbstraction.FileExists(rpath) {
  883. return
  884. }
  885. for _, purpose := range []string{"uploads", "downloads"} {
  886. dir := arozfs.ToSlash(filepath.Join(rpath, purpose))
  887. entries, err := fsh.FileSystemAbstraction.ReadDir(dir)
  888. if err != nil {
  889. continue
  890. }
  891. for _, entry := range entries {
  892. info, err := entry.Info()
  893. if err != nil || time.Since(info.ModTime()) < emailTempMaxAge {
  894. continue
  895. }
  896. fsh.FileSystemAbstraction.RemoveAll(arozfs.ToSlash(filepath.Join(dir, entry.Name())))
  897. }
  898. }
  899. }
  900. func randomHex(n int) string {
  901. buf := make([]byte, n)
  902. if _, err := rand.Read(buf); err != nil {
  903. return fmt.Sprintf("%x", time.Now().UnixNano())
  904. }
  905. return hex.EncodeToString(buf)
  906. }