@@ -34,6 +34,14 @@ type Account struct {
3434 SMIMEKey string `json:"smime_key,omitempty"` // Path to the private key PEM
3535 SMIMESignByDefault bool `json:"smime_sign_by_default,omitempty"` // Whether to enable S/MIME signing by default
3636
37+ // PGP settings
38+ PGPPublicKey string `json:"pgp_public_key,omitempty"` // Path to public key (.asc or .gpg)
39+ PGPPrivateKey string `json:"pgp_private_key,omitempty"` // Path to private key (.asc or .gpg)
40+ PGPKeySource string `json:"pgp_key_source,omitempty"` // "file" (default) or "yubikey" for hardware key
41+ PGPPIN string `json:"-"` // YubiKey PIN (stored in keyring, not JSON)
42+ PGPSignByDefault bool `json:"pgp_sign_by_default,omitempty"` // Auto-sign outgoing emails
43+ PGPEncryptByDefault bool `json:"pgp_encrypt_by_default,omitempty"` // Auto-encrypt when recipient keys available
44+
3745 // OAuth2 settings
3846 AuthMethod string `json:"auth_method,omitempty"` // "password" (default) or "oauth2"
3947
@@ -159,13 +167,17 @@ func configFile() (string, error) {
159167
160168// SaveConfig saves the given configuration to the config file and passwords to the keyring.
161169func SaveConfig (config * Config ) error {
162- // Save passwords to the OS keyring before writing the JSON file
170+ // Save passwords and PGP PINs to the OS keyring before writing the JSON file
163171 for _ , acc := range config .Accounts {
164172 if acc .Password != "" {
165173 // We ignore the error here because some environments (like headless CI)
166174 // might not have a keyring service, but we still want to save the rest of the config.
167175 _ = keyring .Set (keyringServiceName , acc .Email , acc .Password )
168176 }
177+ // Save YubiKey PIN if present
178+ if acc .PGPPIN != "" && acc .PGPKeySource == "yubikey" {
179+ _ = keyring .Set (keyringServiceName , acc .Email + ":pgp-pin" , acc .PGPPIN )
180+ }
169181 }
170182
171183 path , err := configFile ()
@@ -198,25 +210,30 @@ func LoadConfig() (*Config, error) {
198210 var needsMigration bool
199211
200212 type rawAccount struct {
201- ID string `json:"id"`
202- Name string `json:"name"`
203- Email string `json:"email"`
204- Password string `json:"password,omitempty"`
205- ServiceProvider string `json:"service_provider"`
206- FetchEmail string `json:"fetch_email,omitempty"`
207- IMAPServer string `json:"imap_server,omitempty"`
208- IMAPPort int `json:"imap_port,omitempty"`
209- SMTPServer string `json:"smtp_server,omitempty"`
210- SMTPPort int `json:"smtp_port,omitempty"`
211- Insecure bool `json:"insecure,omitempty"`
212- SMIMECert string `json:"smime_cert,omitempty"`
213- SMIMEKey string `json:"smime_key,omitempty"`
214- SMIMESignByDefault bool `json:"smime_sign_by_default,omitempty"`
215- AuthMethod string `json:"auth_method,omitempty"`
216- Protocol string `json:"protocol,omitempty"`
217- JMAPEndpoint string `json:"jmap_endpoint,omitempty"`
218- POP3Server string `json:"pop3_server,omitempty"`
219- POP3Port int `json:"pop3_port,omitempty"`
213+ ID string `json:"id"`
214+ Name string `json:"name"`
215+ Email string `json:"email"`
216+ Password string `json:"password,omitempty"`
217+ ServiceProvider string `json:"service_provider"`
218+ FetchEmail string `json:"fetch_email,omitempty"`
219+ IMAPServer string `json:"imap_server,omitempty"`
220+ IMAPPort int `json:"imap_port,omitempty"`
221+ SMTPServer string `json:"smtp_server,omitempty"`
222+ SMTPPort int `json:"smtp_port,omitempty"`
223+ Insecure bool `json:"insecure,omitempty"`
224+ SMIMECert string `json:"smime_cert,omitempty"`
225+ SMIMEKey string `json:"smime_key,omitempty"`
226+ SMIMESignByDefault bool `json:"smime_sign_by_default,omitempty"`
227+ PGPPublicKey string `json:"pgp_public_key,omitempty"`
228+ PGPPrivateKey string `json:"pgp_private_key,omitempty"`
229+ PGPKeySource string `json:"pgp_key_source,omitempty"`
230+ PGPSignByDefault bool `json:"pgp_sign_by_default,omitempty"`
231+ PGPEncryptByDefault bool `json:"pgp_encrypt_by_default,omitempty"`
232+ AuthMethod string `json:"auth_method,omitempty"`
233+ Protocol string `json:"protocol,omitempty"`
234+ JMAPEndpoint string `json:"jmap_endpoint,omitempty"`
235+ POP3Server string `json:"pop3_server,omitempty"`
236+ POP3Port int `json:"pop3_port,omitempty"`
220237 }
221238 type diskConfig struct {
222239 Accounts []rawAccount `json:"accounts"`
@@ -259,24 +276,29 @@ func LoadConfig() (*Config, error) {
259276 config .MailingLists = raw .MailingLists
260277 for _ , rawAcc := range raw .Accounts {
261278 acc := Account {
262- ID : rawAcc .ID ,
263- Name : rawAcc .Name ,
264- Email : rawAcc .Email ,
265- ServiceProvider : rawAcc .ServiceProvider ,
266- FetchEmail : rawAcc .FetchEmail ,
267- IMAPServer : rawAcc .IMAPServer ,
268- IMAPPort : rawAcc .IMAPPort ,
269- SMTPServer : rawAcc .SMTPServer ,
270- SMTPPort : rawAcc .SMTPPort ,
271- Insecure : rawAcc .Insecure ,
272- SMIMECert : rawAcc .SMIMECert ,
273- SMIMEKey : rawAcc .SMIMEKey ,
274- SMIMESignByDefault : rawAcc .SMIMESignByDefault ,
275- AuthMethod : rawAcc .AuthMethod ,
276- Protocol : rawAcc .Protocol ,
277- JMAPEndpoint : rawAcc .JMAPEndpoint ,
278- POP3Server : rawAcc .POP3Server ,
279- POP3Port : rawAcc .POP3Port ,
279+ ID : rawAcc .ID ,
280+ Name : rawAcc .Name ,
281+ Email : rawAcc .Email ,
282+ ServiceProvider : rawAcc .ServiceProvider ,
283+ FetchEmail : rawAcc .FetchEmail ,
284+ IMAPServer : rawAcc .IMAPServer ,
285+ IMAPPort : rawAcc .IMAPPort ,
286+ SMTPServer : rawAcc .SMTPServer ,
287+ SMTPPort : rawAcc .SMTPPort ,
288+ Insecure : rawAcc .Insecure ,
289+ SMIMECert : rawAcc .SMIMECert ,
290+ SMIMEKey : rawAcc .SMIMEKey ,
291+ SMIMESignByDefault : rawAcc .SMIMESignByDefault ,
292+ PGPPublicKey : rawAcc .PGPPublicKey ,
293+ PGPPrivateKey : rawAcc .PGPPrivateKey ,
294+ PGPKeySource : rawAcc .PGPKeySource ,
295+ PGPSignByDefault : rawAcc .PGPSignByDefault ,
296+ PGPEncryptByDefault : rawAcc .PGPEncryptByDefault ,
297+ AuthMethod : rawAcc .AuthMethod ,
298+ Protocol : rawAcc .Protocol ,
299+ JMAPEndpoint : rawAcc .JMAPEndpoint ,
300+ POP3Server : rawAcc .POP3Server ,
301+ POP3Port : rawAcc .POP3Port ,
280302 }
281303
282304 if rawAcc .Password != "" {
@@ -291,6 +313,13 @@ func LoadConfig() (*Config, error) {
291313 }
292314 }
293315
316+ // Load YubiKey PIN from keyring if using YubiKey
317+ if acc .PGPKeySource == "yubikey" {
318+ if pin , err := keyring .Get (keyringServiceName , acc .Email + ":pgp-pin" ); err == nil {
319+ acc .PGPPIN = pin
320+ }
321+ }
322+
294323 config .Accounts = append (config .Accounts , acc )
295324 }
296325
@@ -329,6 +358,8 @@ func (c *Config) RemoveAccount(id string) bool {
329358 if acc .ID == id {
330359 // Delete password from OS Keyring when account is removed
331360 _ = keyring .Delete (keyringServiceName , acc .Email )
361+ // Delete PGP PIN from OS Keyring if present
362+ _ = keyring .Delete (keyringServiceName , acc .Email + ":pgp-pin" )
332363
333364 c .Accounts = append (c .Accounts [:i ], c .Accounts [i + 1 :]... )
334365 return true
@@ -369,3 +400,13 @@ func (c *Config) GetFirstAccount() *Account {
369400 }
370401 return nil
371402}
403+
404+ // EnsurePGPDir creates the PGP keys directory if it doesn't exist.
405+ func EnsurePGPDir () error {
406+ dir , err := configDir ()
407+ if err != nil {
408+ return err
409+ }
410+ pgpDir := filepath .Join (dir , "pgp" )
411+ return os .MkdirAll (pgpDir , 0700 )
412+ }
0 commit comments