manager.go 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376
  1. package email
  2. /*
  3. manager.go
  4. Manager is the single entry point of the mail backend. It owns the mail
  5. database, the secret box, the IMAP connection pool, pending OAuth sign-ins
  6. and the outbox that delivers delayed (undo-able) and scheduled mail.
  7. The package knows nothing about ArozOS virtual paths: the AGI library
  8. resolves paths, permissions and quota and hands this package plain bytes
  9. and readers.
  10. */
  11. import (
  12. "context"
  13. "crypto/rand"
  14. "encoding/hex"
  15. "errors"
  16. "io"
  17. "net"
  18. "net/http"
  19. "os"
  20. "path/filepath"
  21. "strings"
  22. "sync"
  23. "time"
  24. "github.com/emersion/go-imap/v2"
  25. "github.com/emersion/go-imap/v2/imapclient"
  26. "imuslab.com/arozos/mod/database"
  27. "imuslab.com/arozos/mod/info/logger"
  28. )
  29. // Options configures a Manager.
  30. type Options struct {
  31. //DataDir holds mail.db, secret.key and the outbox spool, e.g. ./system/mail
  32. DataDir string
  33. //Database overrides the bolt file opened under DataDir (tests).
  34. Database KVStore
  35. //HTTPClient is used for OAuth token requests and autoconfig lookups when
  36. //set (tests). Production uses guarded clients.
  37. HTTPClient *http.Client
  38. //Hostname is used in generated Message-IDs when the sender domain is unknown.
  39. Hostname string
  40. //Notify lets the outbox tell a user that a scheduled message failed.
  41. Notify func(username string, title string, message string)
  42. //DisableBackground stops the janitor / outbox goroutine (tests).
  43. DisableBackground bool
  44. }
  45. // Manager runs every mail operation.
  46. type Manager struct {
  47. options Options
  48. store *store
  49. box *secretBox
  50. ownedDB *database.Database
  51. pool *connPool
  52. oauth *oauthManager
  53. previews *previewCache
  54. folders folderCache
  55. outbox *outbox
  56. stop chan struct{}
  57. stopOnce sync.Once
  58. wg sync.WaitGroup
  59. }
  60. // NewManager opens (or creates) the mail data folder and starts the
  61. // background janitor.
  62. func NewManager(options Options) (*Manager, error) {
  63. if strings.TrimSpace(options.DataDir) == "" {
  64. return nil, errors.New("mail data directory is required")
  65. }
  66. if err := os.MkdirAll(options.DataDir, 0700); err != nil {
  67. return nil, err
  68. }
  69. manager := &Manager{
  70. options: options,
  71. pool: newConnPool(),
  72. previews: newPreviewCache(4000),
  73. stop: make(chan struct{}),
  74. }
  75. db := options.Database
  76. if db == nil {
  77. opened, err := database.NewDatabase(filepath.Join(options.DataDir, "mail.db"), false)
  78. if err != nil {
  79. return nil, err
  80. }
  81. manager.ownedDB = opened
  82. db = opened
  83. }
  84. st, err := newStore(db)
  85. if err != nil {
  86. manager.closeDB()
  87. return nil, err
  88. }
  89. manager.store = st
  90. box, err := loadSecretBox(options.DataDir)
  91. if err != nil {
  92. manager.closeDB()
  93. return nil, err
  94. }
  95. manager.box = box
  96. manager.oauth = newOAuthManager(manager)
  97. spool := filepath.Join(options.DataDir, "outbox")
  98. if err := os.MkdirAll(spool, 0700); err != nil {
  99. manager.closeDB()
  100. return nil, err
  101. }
  102. manager.outbox = newOutbox(manager, spool)
  103. if !options.DisableBackground {
  104. manager.wg.Add(1)
  105. go manager.backgroundLoop()
  106. }
  107. return manager, nil
  108. }
  109. // Close stops background work and releases connections and the database.
  110. func (m *Manager) Close() {
  111. m.stopOnce.Do(func() {
  112. close(m.stop)
  113. })
  114. m.wg.Wait()
  115. m.pool.closeAll()
  116. m.closeDB()
  117. }
  118. func (m *Manager) closeDB() {
  119. if m.ownedDB != nil {
  120. m.ownedDB.Close()
  121. m.ownedDB = nil
  122. }
  123. }
  124. func (m *Manager) backgroundLoop() {
  125. defer m.wg.Done()
  126. janitor := time.NewTicker(time.Minute)
  127. outboxTicker := time.NewTicker(5 * time.Second)
  128. defer janitor.Stop()
  129. defer outboxTicker.Stop()
  130. for {
  131. select {
  132. case <-m.stop:
  133. return
  134. case <-janitor.C:
  135. m.pool.reap()
  136. m.oauth.cleanup()
  137. case <-outboxTicker.C:
  138. m.outbox.processDue(context.Background())
  139. }
  140. }
  141. }
  142. // AdminConfig returns the administrator configuration with secrets hidden.
  143. func (m *Manager) AdminConfig() AdminConfigView {
  144. config := m.store.getAdminConfig()
  145. return AdminConfigView{
  146. Google: config.Google.view(),
  147. Microsoft: config.Microsoft.view(),
  148. AllowPrivateHosts: config.AllowPrivateHosts,
  149. AllowInsecure: config.AllowInsecure,
  150. MaxAttachmentMB: config.MaxAttachmentMB,
  151. MaxAccounts: config.MaxAccounts,
  152. }
  153. }
  154. // AdminConfigInput is an update from the admin page. A client secret is only
  155. // replaced when its *SecretSet flag is true (an empty secret then clears it),
  156. // so saving the page without retyping the secret keeps the stored one.
  157. type AdminConfigInput struct {
  158. Google OAuthClient `json:"google"`
  159. Microsoft OAuthClient `json:"microsoft"`
  160. GoogleSecretSet bool `json:"googleSecretSet"`
  161. MicrosoftSecretSet bool `json:"microsoftSecretSet"`
  162. AllowPrivateHosts bool `json:"allowPrivateHosts"`
  163. AllowInsecure bool `json:"allowInsecure"`
  164. MaxAttachmentMB int `json:"maxAttachmentMB"`
  165. MaxAccounts int `json:"maxAccounts"`
  166. }
  167. // SetAdminConfig stores a new administrator configuration. Only admins may
  168. // call it; the AGI layer enforces that and passes the principal along.
  169. func (m *Manager) SetAdminConfig(p Principal, input AdminConfigInput) error {
  170. if !p.Admin {
  171. return errors.New("only administrators can change mail settings")
  172. }
  173. current := m.store.getAdminConfig()
  174. update := func(stored OAuthClient, incoming OAuthClient, secretSet bool) (OAuthClient, error) {
  175. result := incoming
  176. result.ClientSecret = stored.ClientSecret
  177. if secretSet {
  178. sealed, err := m.box.Seal(strings.TrimSpace(incoming.ClientSecret))
  179. if err != nil {
  180. return result, err
  181. }
  182. result.ClientSecret = sealed
  183. }
  184. return result, nil
  185. }
  186. google, err := update(current.Google, input.Google, input.GoogleSecretSet)
  187. if err != nil {
  188. return err
  189. }
  190. microsoft, err := update(current.Microsoft, input.Microsoft, input.MicrosoftSecretSet)
  191. if err != nil {
  192. return err
  193. }
  194. next := AdminConfig{
  195. Google: google,
  196. Microsoft: microsoft,
  197. AllowPrivateHosts: input.AllowPrivateHosts,
  198. AllowInsecure: input.AllowInsecure,
  199. MaxAttachmentMB: input.MaxAttachmentMB,
  200. MaxAccounts: input.MaxAccounts,
  201. }
  202. if err := m.store.saveAdminConfig(next); err != nil {
  203. return err
  204. }
  205. m.oauth.resetTokenCache()
  206. logger.PrintAndLog("Email", "Mail administrator settings updated by "+p.Username, nil)
  207. return nil
  208. }
  209. // Settings returns a user's preferences.
  210. func (m *Manager) Settings(p Principal) UserSettings {
  211. return m.store.getSettings(p.Username)
  212. }
  213. // SaveSettings replaces a user's preferences.
  214. func (m *Manager) SaveSettings(p Principal, settings UserSettings) (UserSettings, error) {
  215. settings.normalise()
  216. if err := m.store.saveSettings(p.Username, settings); err != nil {
  217. return settings, err
  218. }
  219. return settings, nil
  220. }
  221. // TrustSender adds an address or domain to the remote-content allow list.
  222. func (m *Manager) TrustSender(p Principal, sender string) error {
  223. sender = strings.ToLower(strings.TrimSpace(sender))
  224. if sender == "" {
  225. return errors.New("sender is empty")
  226. }
  227. m.store.mutex.Lock()
  228. defer m.store.mutex.Unlock()
  229. settings := m.store.getSettings(p.Username)
  230. if settings.IsTrustedSender(sender) {
  231. return nil
  232. }
  233. settings.TrustedSenders = append(settings.TrustedSenders, sender)
  234. return m.store.saveSettings(p.Username, settings)
  235. }
  236. // allowPrivate decides whether p may reach LAN / loopback servers.
  237. func (m *Manager) allowPrivate(p Principal) bool {
  238. if p.Admin {
  239. return true
  240. }
  241. return m.store.getAdminConfig().AllowPrivateHosts
  242. }
  243. // allowInsecure decides whether p may use unencrypted connections.
  244. func (m *Manager) allowInsecure(p Principal) bool {
  245. if p.Admin {
  246. return true
  247. }
  248. return m.store.getAdminConfig().AllowInsecure
  249. }
  250. // maxAttachmentBytes is the configured outgoing attachment budget.
  251. func (m *Manager) maxAttachmentBytes() int64 {
  252. return int64(m.store.getAdminConfig().MaxAttachmentMB) * 1024 * 1024
  253. }
  254. func poolKey(owner string, accountID string) string {
  255. return owner + "/" + accountID
  256. }
  257. // withIMAP runs fn on a pooled, authenticated connection of an account.
  258. func (m *Manager) withIMAP(ctx context.Context, p Principal, account *Account, fn func(c *imapclient.Client) error) error {
  259. key := poolKey(p.Username, account.ID)
  260. conn, err := m.pool.acquire(ctx, key, func() (*imapclient.Client, error) {
  261. return m.dialIMAP(ctx, p, account)
  262. })
  263. if err != nil {
  264. m.noteAuthError(p, account, err)
  265. return err
  266. }
  267. err = fn(conn.client)
  268. m.pool.release(key, conn, connectionBroken(conn.client, err))
  269. return err
  270. }
  271. // connectionBroken decides whether a connection may go back to the pool.
  272. // Protocol-level "NO"/"BAD" answers and our own lookup errors leave it
  273. // usable; network failures and closed connections do not.
  274. func connectionBroken(client *imapclient.Client, err error) bool {
  275. if isClosed(client) {
  276. return true
  277. }
  278. if err == nil {
  279. return false
  280. }
  281. var statusErr *imap.Error
  282. if errors.As(err, &statusErr) {
  283. return false
  284. }
  285. var netErr net.Error
  286. if errors.As(err, &netErr) || errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
  287. return true
  288. }
  289. message := err.Error()
  290. return strings.Contains(message, "closed network connection") || strings.HasPrefix(message, "imapclient:") ||
  291. strings.Contains(message, "in imapwire")
  292. }
  293. // noteAuthError records (or clears) the authentication problem shown on an
  294. // account in the sidebar.
  295. func (m *Manager) noteAuthError(p Principal, account *Account, err error) {
  296. if !IsAuthError(err) {
  297. return
  298. }
  299. m.store.mutex.Lock()
  300. defer m.store.mutex.Unlock()
  301. stored, gerr := m.store.getAccount(p.Username, account.ID)
  302. if gerr != nil {
  303. return
  304. }
  305. stored.AuthError = err.Error()
  306. m.store.saveAccount(stored)
  307. }
  308. func (m *Manager) clearAuthError(p Principal, account *Account) {
  309. if account.AuthError == "" {
  310. return
  311. }
  312. m.store.mutex.Lock()
  313. defer m.store.mutex.Unlock()
  314. stored, err := m.store.getAccount(p.Username, account.ID)
  315. if err != nil {
  316. return
  317. }
  318. stored.AuthError = ""
  319. m.store.saveAccount(stored)
  320. account.AuthError = ""
  321. }
  322. func (m *Manager) now() time.Time {
  323. return time.Now()
  324. }
  325. // randomID returns n random bytes as hex.
  326. func randomID(n int) string {
  327. buf := make([]byte, n)
  328. if _, err := rand.Read(buf); err != nil {
  329. return hex.EncodeToString([]byte(time.Now().Format("150405.000000")))
  330. }
  331. return hex.EncodeToString(buf)
  332. }