1package config
2
3import (
4 "encoding/json"
5 "fmt"
6 "log"
7 "os"
8 "path/filepath"
9 "strings"
10
11 "github.com/google/uuid"
12 "github.com/zalando/go-keyring"
13)
14
15const keyringServiceName = "matcha-email-client"
16
17// Date format presets use human-readable tokens. Supported tokens:
18//
19// YYYY (4-digit year), YY (2-digit year)
20// MM (month, or minutes when following an hour token + colon)
21// mm (minutes, explicit)
22// DD (day)
23// HH (24-hour), hh (12-hour, zero-padded)
24// SS, ss (seconds)
25// AM, PM (meridiem marker)
26const (
27 DateFormatISO = "YYYY-MM-DD HH:MM"
28 DateFormatUS = "MM/DD/YYYY hh:MM AM"
29 DateFormatEU = "DD/MM/YYYY HH:MM"
30)
31
32// Account stores the configuration for a single email account.
33type Account struct {
34 ID string `json:"id"`
35 Name string `json:"name"`
36 Email string `json:"email"`
37 Password string `json:"-"` // "-" prevents the password from being saved to config.json
38 ServiceProvider string `json:"service_provider"` // "gmail", "outlook", "icloud", or "custom"
39 // FetchEmail is the single email address for which messages should be fetched.
40 // If empty, it will default to `Email` when accounts are added.
41 FetchEmail string `json:"fetch_email,omitempty"`
42 // SendAsEmail controls the visible From header on outgoing mail.
43 // If empty, it defaults to FetchEmail, then Email.
44 SendAsEmail string `json:"send_as_email,omitempty"`
45
46 // Custom server settings (used when ServiceProvider is "custom")
47 IMAPServer string `json:"imap_server,omitempty"`
48 IMAPPort int `json:"imap_port,omitempty"`
49 SMTPServer string `json:"smtp_server,omitempty"`
50 SMTPPort int `json:"smtp_port,omitempty"`
51 Insecure bool `json:"insecure,omitempty"`
52
53 // S/MIME settings
54 SMIMECert string `json:"smime_cert,omitempty"` // Path to the public certificate PEM
55 SMIMEKey string `json:"smime_key,omitempty"` // Path to the private key PEM
56 SMIMESignByDefault bool `json:"smime_sign_by_default,omitempty"` // Whether to enable S/MIME signing by default
57
58 // PGP settings
59 PGPPublicKey string `json:"pgp_public_key,omitempty"` // Path to public key (.asc or .gpg)
60 PGPPrivateKey string `json:"pgp_private_key,omitempty"` // Path to private key (.asc or .gpg)
61 PGPKeySource string `json:"pgp_key_source,omitempty"` // "file" (default) or "yubikey" for hardware key
62 PGPPIN string `json:"-"` // YubiKey PIN (stored in keyring, not JSON)
63 PGPSignByDefault bool `json:"pgp_sign_by_default,omitempty"` // Auto-sign outgoing emails
64
65 // OAuth2 settings
66 AuthMethod string `json:"auth_method,omitempty"` // "password" (default) or "oauth2"
67
68 // Multi-protocol settings
69 Protocol string `json:"protocol,omitempty"` // "imap" (default), "jmap", or "pop3"
70 JMAPEndpoint string `json:"jmap_endpoint,omitempty"` // JMAP session URL (for protocol=jmap)
71 POP3Server string `json:"pop3_server,omitempty"` // POP3 server hostname (for protocol=pop3)
72 POP3Port int `json:"pop3_port,omitempty"` // POP3 server port (for protocol=pop3)
73
74 // Per-account signature (overrides global signature)
75 Signature string `json:"signature,omitempty"`
76}
77
78// MailingList represents a named group of email addresses.
79type MailingList struct {
80 Name string `json:"name"`
81 Addresses []string `json:"addresses"`
82}
83
84// Config stores the user's email configuration with multiple accounts.
85type Config struct {
86 Accounts []Account `json:"accounts"`
87 DisableImages bool `json:"disable_images,omitempty"`
88 HideTips bool `json:"hide_tips,omitempty"`
89 DisableNotifications bool `json:"disable_notifications,omitempty"`
90 EnableSplitPane bool `json:"enable_split_pane,omitempty"`
91 Theme string `json:"theme,omitempty"`
92 MailingLists []MailingList `json:"mailing_lists,omitempty"`
93 DateFormat string `json:"date_format,omitempty"`
94 Language string `json:"language,omitempty"` // Language code (e.g., "en", "es", "de")
95}
96
97// GetDateFormat returns the Go time reference layout translated from the
98// user's configured human-readable format. Defaults to EU when unset.
99func (c *Config) GetDateFormat() string {
100 f := c.DateFormat
101 if f == "" {
102 f = DateFormatEU
103 }
104 return translateDateFormat(f)
105}
106
107// GetLanguage returns the configured language code, defaulting to "en".
108func (c *Config) GetLanguage() string {
109 if c.Language == "" {
110 return "en"
111 }
112 return c.Language
113}
114
115// translateDateFormat converts a human-readable format string (e.g.
116// "DD/MM/YYYY HH:MM") into a Go reference-time layout usable by
117// time.Format. MM is disambiguated by context: when it directly follows
118// an hour token plus ":", it maps to minutes; otherwise to month.
119func translateDateFormat(f string) string {
120 var b strings.Builder
121 i := 0
122 for i < len(f) {
123 rest := f[i:]
124 switch {
125 case strings.HasPrefix(rest, "YYYY"):
126 b.WriteString("2006")
127 i += 4
128 case strings.HasPrefix(rest, "YY"):
129 b.WriteString("06")
130 i += 2
131 case strings.HasPrefix(rest, "DD"):
132 b.WriteString("02")
133 i += 2
134 case strings.HasPrefix(rest, "HH"):
135 b.WriteString("15")
136 i += 2
137 case strings.HasPrefix(rest, "hh"):
138 b.WriteString("03")
139 i += 2
140 case strings.HasPrefix(rest, "mm"):
141 b.WriteString("04")
142 i += 2
143 case strings.HasPrefix(rest, "SS"), strings.HasPrefix(rest, "ss"):
144 b.WriteString("05")
145 i += 2
146 case strings.HasPrefix(rest, "MM"):
147 cur := b.String()
148 if strings.HasSuffix(cur, "15:") || strings.HasSuffix(cur, "03:") {
149 b.WriteString("04")
150 } else {
151 b.WriteString("01")
152 }
153 i += 2
154 case strings.HasPrefix(rest, "AM"), strings.HasPrefix(rest, "PM"):
155 b.WriteString("PM")
156 i += 2
157 default:
158 b.WriteByte(f[i])
159 i++
160 }
161 }
162 return b.String()
163}
164
165// GetIMAPServer returns the IMAP server address for the account.
166func (a *Account) GetIMAPServer() string {
167 switch a.ServiceProvider {
168 case "gmail":
169 return "imap.gmail.com"
170 case "outlook":
171 return "outlook.office365.com"
172 case "icloud":
173 return "imap.mail.me.com"
174 case "custom":
175 return a.IMAPServer
176 default:
177 return ""
178 }
179}
180
181// GetIMAPPort returns the IMAP port for the account.
182func (a *Account) GetIMAPPort() int {
183 switch a.ServiceProvider {
184 case "gmail", "outlook", "icloud":
185 return 993
186 case "custom":
187 if a.IMAPPort != 0 {
188 return a.IMAPPort
189 }
190 return 993 // Default IMAP SSL port
191 default:
192 return 993
193 }
194}
195
196// GetSMTPServer returns the SMTP server address for the account.
197func (a *Account) GetSMTPServer() string {
198 switch a.ServiceProvider {
199 case "gmail":
200 return "smtp.gmail.com"
201 case "outlook":
202 return "smtp.office365.com"
203 case "icloud":
204 return "smtp.mail.me.com"
205 case "custom":
206 return a.SMTPServer
207 default:
208 return ""
209 }
210}
211
212// GetSMTPPort returns the SMTP port for the account.
213func (a *Account) GetSMTPPort() int {
214 switch a.ServiceProvider {
215 case "gmail", "outlook", "icloud":
216 return 587
217 case "custom":
218 if a.SMTPPort != 0 {
219 return a.SMTPPort
220 }
221 return 587 // Default SMTP TLS port
222 default:
223 return 587
224 }
225}
226
227// GetFetchEmail returns the configured fetch identity, falling back to Email.
228func (a *Account) GetFetchEmail() string {
229 if a.FetchEmail != "" {
230 return a.FetchEmail
231 }
232 return a.Email
233}
234
235// GetSendAsEmail returns the visible sender address for outgoing mail.
236func (a *Account) GetSendAsEmail() string {
237 if a.SendAsEmail != "" {
238 return a.SendAsEmail
239 }
240 return a.GetFetchEmail()
241}
242
243// FormatFromHeader returns the display-ready From header value.
244func (a *Account) FormatFromHeader() string {
245 sendAs := a.GetSendAsEmail()
246 if a.Name != "" && sendAs != "" {
247 return fmt.Sprintf("%s <%s>", a.Name, sendAs)
248 }
249 return sendAs
250}
251
252// GetPOP3Server returns the POP3 server address for the account.
253func (a *Account) GetPOP3Server() string {
254 if a.POP3Server != "" {
255 return a.POP3Server
256 }
257 return ""
258}
259
260// GetPOP3Port returns the POP3 port for the account.
261func (a *Account) GetPOP3Port() int {
262 if a.POP3Port != 0 {
263 return a.POP3Port
264 }
265 return 995 // Default POP3 SSL port
266}
267
268// GetConfigDir returns the path to the configuration directory (exported).
269func GetConfigDir() (string, error) {
270 return configDir()
271}
272
273// configDir returns the path to the configuration directory (internal).
274func configDir() (string, error) {
275 home, err := os.UserHomeDir()
276 if err != nil {
277 return "", err
278 }
279 return filepath.Join(home, ".config", "matcha"), nil
280}
281
282// GetCacheDir returns the path to the cache directory (exported).
283func GetCacheDir() (string, error) {
284 return cacheDir()
285}
286
287// cacheDir returns the path to the cache directory (internal).
288func cacheDir() (string, error) {
289 home, err := os.UserHomeDir()
290 if err != nil {
291 return "", err
292 }
293 return filepath.Join(home, ".cache", "matcha"), nil
294}
295
296// MigrateCacheFiles moves cache files from ~/.config/matcha/ to ~/.cache/matcha/ if needed.
297// This is a one-time migration for existing installations.
298func MigrateCacheFiles() error {
299 src, err := configDir()
300 if err != nil {
301 return err
302 }
303 dst, err := cacheDir()
304 if err != nil {
305 return err
306 }
307 if err := os.MkdirAll(dst, 0700); err != nil {
308 return err
309 }
310
311 // Files to migrate
312 files := []string{"email_cache.json", "contacts.json", "drafts.json", "folder_cache.json"}
313 for _, f := range files {
314 oldPath := filepath.Join(src, f)
315 newPath := filepath.Join(dst, f)
316 if _, err := os.Stat(oldPath); err == nil {
317 // Only migrate if destination doesn't already exist
318 if _, err := os.Stat(newPath); err != nil {
319 if err := os.Rename(oldPath, newPath); err != nil {
320 return err
321 }
322 }
323 }
324 }
325
326 // Migrate folder_emails directory
327 oldDir := filepath.Join(src, "folder_emails")
328 newDir := filepath.Join(dst, "folder_emails")
329 if info, err := os.Stat(oldDir); err == nil && info.IsDir() {
330 if _, err := os.Stat(newDir); err != nil {
331 if err := os.Rename(oldDir, newDir); err != nil {
332 return err
333 }
334 }
335 }
336
337 return nil
338}
339
340// configFile returns the full path to the configuration file.
341func configFile() (string, error) {
342 dir, err := configDir()
343 if err != nil {
344 return "", err
345 }
346 return filepath.Join(dir, "config.json"), nil
347}
348
349// secureDiskAccount includes the Password field in JSON when secure mode is active.
350type secureDiskAccount struct {
351 ID string `json:"id"`
352 Name string `json:"name"`
353 Email string `json:"email"`
354 Password string `json:"password,omitempty"`
355 ServiceProvider string `json:"service_provider"`
356 FetchEmail string `json:"fetch_email,omitempty"`
357 SendAsEmail string `json:"send_as_email,omitempty"`
358 IMAPServer string `json:"imap_server,omitempty"`
359 IMAPPort int `json:"imap_port,omitempty"`
360 SMTPServer string `json:"smtp_server,omitempty"`
361 SMTPPort int `json:"smtp_port,omitempty"`
362 Insecure bool `json:"insecure,omitempty"`
363 SMIMECert string `json:"smime_cert,omitempty"`
364 SMIMEKey string `json:"smime_key,omitempty"`
365 SMIMESignByDefault bool `json:"smime_sign_by_default,omitempty"`
366 PGPPublicKey string `json:"pgp_public_key,omitempty"`
367 PGPPrivateKey string `json:"pgp_private_key,omitempty"`
368 PGPKeySource string `json:"pgp_key_source,omitempty"`
369 PGPPIN string `json:"pgp_pin,omitempty"`
370 PGPSignByDefault bool `json:"pgp_sign_by_default,omitempty"`
371 AuthMethod string `json:"auth_method,omitempty"`
372 Protocol string `json:"protocol,omitempty"`
373 JMAPEndpoint string `json:"jmap_endpoint,omitempty"`
374 POP3Server string `json:"pop3_server,omitempty"`
375 POP3Port int `json:"pop3_port,omitempty"`
376}
377
378type secureDiskConfig struct {
379 Accounts []secureDiskAccount `json:"accounts"`
380 DisableImages bool `json:"disable_images,omitempty"`
381 HideTips bool `json:"hide_tips,omitempty"`
382 DisableNotifications bool `json:"disable_notifications,omitempty"`
383 EnableSplitPane bool `json:"enable_split_pane,omitempty"`
384 Theme string `json:"theme,omitempty"`
385 MailingLists []MailingList `json:"mailing_lists,omitempty"`
386 DateFormat string `json:"date_format,omitempty"`
387}
388
389// SaveConfig saves the given configuration to the config file and passwords to the keyring.
390func SaveConfig(config *Config) error {
391 secureMode := GetSessionKey() != nil
392
393 if !secureMode {
394 // Save passwords and PGP PINs to the OS keyring before writing the JSON file.
395 // A silent keyring failure here would lose the credential on restart without
396 // any hint to the user. Log the error as a warning so the misconfiguration
397 // (no keyring backend, locked keyring, etc.) is at least visible. See #616.
398 for _, acc := range config.Accounts {
399 if acc.Password != "" {
400 if err := keyring.Set(keyringServiceName, acc.Email, acc.Password); err != nil {
401 log.Printf("matcha: failed to store password for %s in keyring: %v", acc.Email, err)
402 }
403 }
404 if acc.PGPPIN != "" && acc.PGPKeySource == "yubikey" {
405 if err := keyring.Set(keyringServiceName, acc.Email+":pgp-pin", acc.PGPPIN); err != nil {
406 log.Printf("matcha: failed to store PGP PIN for %s in keyring: %v", acc.Email, err)
407 }
408 }
409 }
410 }
411
412 path, err := configFile()
413 if err != nil {
414 return err
415 }
416 if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
417 return err
418 }
419
420 var data []byte
421 if secureMode {
422 // In secure mode, include passwords in the JSON (they'll be encrypted on disk)
423 sdc := secureDiskConfig{
424 DisableImages: config.DisableImages,
425 HideTips: config.HideTips,
426 DisableNotifications: config.DisableNotifications,
427 EnableSplitPane: config.EnableSplitPane,
428 Theme: config.Theme,
429 MailingLists: config.MailingLists,
430 DateFormat: config.DateFormat,
431 }
432 for _, acc := range config.Accounts {
433 sdc.Accounts = append(sdc.Accounts, secureDiskAccount{
434 ID: acc.ID,
435 Name: acc.Name,
436 Email: acc.Email,
437 Password: acc.Password,
438 ServiceProvider: acc.ServiceProvider,
439 FetchEmail: acc.FetchEmail,
440 SendAsEmail: acc.SendAsEmail,
441 IMAPServer: acc.IMAPServer,
442 IMAPPort: acc.IMAPPort,
443 SMTPServer: acc.SMTPServer,
444 SMTPPort: acc.SMTPPort,
445 Insecure: acc.Insecure,
446 SMIMECert: acc.SMIMECert,
447 SMIMEKey: acc.SMIMEKey,
448 SMIMESignByDefault: acc.SMIMESignByDefault,
449 PGPPublicKey: acc.PGPPublicKey,
450 PGPPrivateKey: acc.PGPPrivateKey,
451 PGPKeySource: acc.PGPKeySource,
452 PGPPIN: acc.PGPPIN,
453 PGPSignByDefault: acc.PGPSignByDefault,
454 AuthMethod: acc.AuthMethod,
455 Protocol: acc.Protocol,
456 JMAPEndpoint: acc.JMAPEndpoint,
457 POP3Server: acc.POP3Server,
458 POP3Port: acc.POP3Port,
459 })
460 }
461 data, err = json.MarshalIndent(sdc, "", " ")
462 } else {
463 data, err = json.MarshalIndent(config, "", " ")
464 }
465 if err != nil {
466 return err
467 }
468 return SecureWriteFile(path, data, 0600)
469}
470
471// LoadConfig loads the configuration from the config file and passwords from the keyring.
472// It automatically migrates plain-text passwords to the OS keyring if they exist.
473func LoadConfig() (*Config, error) {
474 path, err := configFile()
475 if err != nil {
476 return nil, err
477 }
478
479 if dir, err := configDir(); err == nil {
480 if err := LoadKeybindsFromDir(dir); err != nil {
481 log.Printf("matcha: keybinds load error (using defaults): %v", err)
482 }
483 }
484 data, err := SecureReadFile(path)
485 if err != nil {
486 return nil, err
487 }
488
489 secureMode := GetSessionKey() != nil
490
491 var config Config
492 var needsMigration bool
493
494 type rawAccount struct {
495 ID string `json:"id"`
496 Name string `json:"name"`
497 Email string `json:"email"`
498 Password string `json:"password,omitempty"`
499 ServiceProvider string `json:"service_provider"`
500 FetchEmail string `json:"fetch_email,omitempty"`
501 SendAsEmail string `json:"send_as_email,omitempty"`
502 IMAPServer string `json:"imap_server,omitempty"`
503 IMAPPort int `json:"imap_port,omitempty"`
504 SMTPServer string `json:"smtp_server,omitempty"`
505 SMTPPort int `json:"smtp_port,omitempty"`
506 Insecure bool `json:"insecure,omitempty"`
507 SMIMECert string `json:"smime_cert,omitempty"`
508 SMIMEKey string `json:"smime_key,omitempty"`
509 SMIMESignByDefault bool `json:"smime_sign_by_default,omitempty"`
510 PGPPublicKey string `json:"pgp_public_key,omitempty"`
511 PGPPrivateKey string `json:"pgp_private_key,omitempty"`
512 PGPKeySource string `json:"pgp_key_source,omitempty"`
513 PGPPIN string `json:"pgp_pin,omitempty"`
514 PGPSignByDefault bool `json:"pgp_sign_by_default,omitempty"`
515 AuthMethod string `json:"auth_method,omitempty"`
516 Protocol string `json:"protocol,omitempty"`
517 JMAPEndpoint string `json:"jmap_endpoint,omitempty"`
518 POP3Server string `json:"pop3_server,omitempty"`
519 POP3Port int `json:"pop3_port,omitempty"`
520 }
521 type diskConfig struct {
522 Accounts []rawAccount `json:"accounts"`
523 DisableImages bool `json:"disable_images,omitempty"`
524 HideTips bool `json:"hide_tips,omitempty"`
525 DisableNotifications bool `json:"disable_notifications,omitempty"`
526 EnableSplitPane bool `json:"enable_split_pane,omitempty"`
527 Theme string `json:"theme,omitempty"`
528 MailingLists []MailingList `json:"mailing_lists,omitempty"`
529 DateFormat string `json:"date_format,omitempty"`
530 Language string `json:"language,omitempty"`
531 }
532
533 var raw diskConfig
534 if err := json.Unmarshal(data, &raw); err != nil {
535 var legacyConfig legacyConfigFormat
536 if legacyErr := json.Unmarshal(data, &legacyConfig); legacyErr == nil && legacyConfig.Email != "" {
537 config = Config{
538 Accounts: []Account{
539 {
540 ID: uuid.New().String(),
541 Name: legacyConfig.Name,
542 Email: legacyConfig.Email,
543 Password: legacyConfig.Password,
544 ServiceProvider: legacyConfig.ServiceProvider,
545 FetchEmail: legacyConfig.Email,
546 },
547 },
548 }
549 // SaveConfig automatically pushes the password to the keyring and strips it from JSON
550 if saveErr := SaveConfig(&config); saveErr != nil {
551 return nil, saveErr
552 }
553 return &config, nil
554 }
555 return nil, err
556 }
557
558 config.DisableImages = raw.DisableImages
559 config.HideTips = raw.HideTips
560 config.DisableNotifications = raw.DisableNotifications
561 config.EnableSplitPane = raw.EnableSplitPane
562 config.Theme = raw.Theme
563 config.MailingLists = raw.MailingLists
564 config.DateFormat = raw.DateFormat
565 config.Language = raw.Language
566 for _, rawAcc := range raw.Accounts {
567 acc := Account{
568 ID: rawAcc.ID,
569 Name: rawAcc.Name,
570 Email: rawAcc.Email,
571 ServiceProvider: rawAcc.ServiceProvider,
572 FetchEmail: rawAcc.FetchEmail,
573 SendAsEmail: rawAcc.SendAsEmail,
574 IMAPServer: rawAcc.IMAPServer,
575 IMAPPort: rawAcc.IMAPPort,
576 SMTPServer: rawAcc.SMTPServer,
577 SMTPPort: rawAcc.SMTPPort,
578 Insecure: rawAcc.Insecure,
579 SMIMECert: rawAcc.SMIMECert,
580 SMIMEKey: rawAcc.SMIMEKey,
581 SMIMESignByDefault: rawAcc.SMIMESignByDefault,
582 PGPPublicKey: rawAcc.PGPPublicKey,
583 PGPPrivateKey: rawAcc.PGPPrivateKey,
584 PGPKeySource: rawAcc.PGPKeySource,
585 PGPSignByDefault: rawAcc.PGPSignByDefault,
586 AuthMethod: rawAcc.AuthMethod,
587 Protocol: rawAcc.Protocol,
588 JMAPEndpoint: rawAcc.JMAPEndpoint,
589 POP3Server: rawAcc.POP3Server,
590 POP3Port: rawAcc.POP3Port,
591 }
592
593 // Validate PGPKeySource
594 if acc.PGPKeySource != "" && acc.PGPKeySource != "file" && acc.PGPKeySource != "yubikey" {
595 return nil, fmt.Errorf("account %q: invalid pgp_key_source %q (must be \"file\" or \"yubikey\")", acc.Name, acc.PGPKeySource)
596 }
597
598 if secureMode {
599 // In secure mode, passwords and PINs are stored in the encrypted config JSON
600 acc.Password = rawAcc.Password
601 acc.PGPPIN = rawAcc.PGPPIN
602 } else if rawAcc.Password != "" {
603 // Found a plain-text password! Move it to the OS Keyring.
604 if err := keyring.Set(keyringServiceName, rawAcc.Email, rawAcc.Password); err != nil {
605 log.Printf("matcha: failed to migrate password for %s into keyring: %v", rawAcc.Email, err)
606 }
607 acc.Password = rawAcc.Password
608 needsMigration = true
609 } else {
610 // No plaintext password in JSON, fetch from Keyring as normal.
611 if pwd, err := keyring.Get(keyringServiceName, acc.Email); err == nil {
612 acc.Password = pwd
613 }
614 }
615
616 if !secureMode {
617 // Load YubiKey PIN from keyring if using YubiKey
618 if acc.PGPKeySource == "yubikey" {
619 if pin, err := keyring.Get(keyringServiceName, acc.Email+":pgp-pin"); err == nil {
620 acc.PGPPIN = pin
621 }
622 }
623 }
624
625 config.Accounts = append(config.Accounts, acc)
626 }
627
628 if needsMigration {
629 if saveErr := SaveConfig(&config); saveErr != nil {
630 return nil, saveErr
631 }
632 }
633
634 return &config, nil
635}
636
637// legacyConfigFormat represents the old single-account configuration format.
638type legacyConfigFormat struct {
639 ServiceProvider string `json:"service_provider"`
640 Email string `json:"email"`
641 Password string `json:"password"`
642 Name string `json:"name"`
643}
644
645// AddAccount adds a new account to the configuration.
646func (c *Config) AddAccount(account Account) {
647 if account.ID == "" {
648 account.ID = uuid.New().String()
649 }
650 // Ensure FetchEmail defaults to the login Email if not explicitly set.
651 if account.FetchEmail == "" && account.Email != "" {
652 account.FetchEmail = account.Email
653 }
654 c.Accounts = append(c.Accounts, account)
655}
656
657// RemoveAccount removes an account by its ID and deletes its password from the keyring.
658func (c *Config) RemoveAccount(id string) bool {
659 for i, acc := range c.Accounts {
660 if acc.ID == id {
661 // Delete password from OS Keyring when account is removed. A
662 // missing entry is expected and not worth logging (keyring.Get is
663 // what we rely on elsewhere to detect that), but any other error
664 // means we failed to clean up a still-reachable secret.
665 if err := keyring.Delete(keyringServiceName, acc.Email); err != nil && err != keyring.ErrNotFound {
666 log.Printf("matcha: failed to delete password for %s from keyring: %v", acc.Email, err)
667 }
668 // Delete PGP PIN from OS Keyring if present
669 if err := keyring.Delete(keyringServiceName, acc.Email+":pgp-pin"); err != nil && err != keyring.ErrNotFound {
670 log.Printf("matcha: failed to delete PGP PIN for %s from keyring: %v", acc.Email, err)
671 }
672
673 c.Accounts = append(c.Accounts[:i], c.Accounts[i+1:]...)
674 return true
675 }
676 }
677 return false
678}
679
680// GetAccountByID returns an account by its ID.
681func (c *Config) GetAccountByID(id string) *Account {
682 for i := range c.Accounts {
683 if c.Accounts[i].ID == id {
684 return &c.Accounts[i]
685 }
686 }
687 return nil
688}
689
690// GetAccountByEmail returns an account by its email address.
691func (c *Config) GetAccountByEmail(email string) *Account {
692 for i := range c.Accounts {
693 if c.Accounts[i].Email == email {
694 return &c.Accounts[i]
695 }
696 }
697 return nil
698}
699
700// HasAccounts returns true if there are any configured accounts.
701func (c *Config) HasAccounts() bool {
702 return len(c.Accounts) > 0
703}
704
705// GetFirstAccount returns the first account or nil if none exist.
706func (c *Config) GetFirstAccount() *Account {
707 if len(c.Accounts) > 0 {
708 return &c.Accounts[0]
709 }
710 return nil
711}
712
713// EnsurePGPDir creates the PGP keys directory if it doesn't exist.
714func EnsurePGPDir() error {
715 dir, err := configDir()
716 if err != nil {
717 return err
718 }
719 pgpDir := filepath.Join(dir, "pgp")
720 return os.MkdirAll(pgpDir, 0700)
721}