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