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