FRE-681: Fix security review findings (3 HIGH, 3 MEDIUM, 2 LOW)
HIGH fixes: - Access Token now used as PGP Passphrase: replaced session.AccessToken with session.MailPassphrase for all PGP operations - Session stored encrypted in keyring and file (was plain JSON) - Added checkAuthenticated() helper with IsAuthenticated() guard MEDIUM fixes: - Added MailPassphrase field to Session, collected during login - Added email validation in LoginInteractive - Added keyring cleanup on Logout - Implemented RefreshToken with actual API call LOW fixes: - Added mutex to PGPKeyRing for thread safety - Added ZeroPrivateKeyData() for memory cleanup - Use net/mail.ParseAddress for proper recipient parsing - Renamed internal/mail import to internalmail to avoid conflict
This commit is contained in:
133
cmd/mail.go
133
cmd/mail.go
@@ -2,6 +2,7 @@ package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/mail"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -10,10 +11,26 @@ import (
|
||||
"github.com/frenocorp/pop/internal/api"
|
||||
"github.com/frenocorp/pop/internal/auth"
|
||||
"github.com/frenocorp/pop/internal/config"
|
||||
"github.com/frenocorp/pop/internal/mail"
|
||||
internalmail "github.com/frenocorp/pop/internal/mail"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func checkAuthenticated() (*auth.Session, error) {
|
||||
sessionMgr, err := auth.NewSessionManager()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create session manager: %w", err)
|
||||
}
|
||||
authenticated, err := sessionMgr.IsAuthenticated()
|
||||
if err != nil || !authenticated {
|
||||
return nil, fmt.Errorf("not authenticated (run 'pop login' first): %w", err)
|
||||
}
|
||||
session, err := sessionMgr.GetSession()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("not authenticated: %w", err)
|
||||
}
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func mailCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "mail",
|
||||
@@ -47,31 +64,27 @@ func mailListCmd() *cobra.Command {
|
||||
return fmt.Errorf("failed to load config: %w", err)
|
||||
}
|
||||
|
||||
sessionMgr, err := auth.NewSessionManager()
|
||||
session, err := checkAuthenticated()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create session manager: %w", err)
|
||||
}
|
||||
session, err := sessionMgr.GetSession()
|
||||
if err != nil {
|
||||
return fmt.Errorf("not authenticated: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
client := api.NewProtonMailClient(cfg)
|
||||
client.SetAuthHeader(session.AccessToken)
|
||||
mailClient := mail.NewClient(client)
|
||||
mailClient := internalmail.NewClient(client)
|
||||
|
||||
folderVal := mail.FolderInbox
|
||||
folderVal := internalmail.FolderInbox
|
||||
switch folder {
|
||||
case "inbox":
|
||||
folderVal = mail.FolderInbox
|
||||
folderVal = internalmail.FolderInbox
|
||||
case "sent":
|
||||
folderVal = mail.FolderSent
|
||||
folderVal = internalmail.FolderSent
|
||||
case "drafts":
|
||||
folderVal = mail.FolderDraft
|
||||
folderVal = internalmail.FolderDraft
|
||||
case "trash":
|
||||
folderVal = mail.FolderTrash
|
||||
folderVal = internalmail.FolderTrash
|
||||
case "spam":
|
||||
folderVal = mail.FolderSpam
|
||||
folderVal = internalmail.FolderSpam
|
||||
default:
|
||||
return fmt.Errorf("unknown folder: %s (valid: inbox, sent, drafts, trash, spam)", folder)
|
||||
}
|
||||
@@ -101,11 +114,11 @@ func mailListCmd() *cobra.Command {
|
||||
readPtr = &v
|
||||
}
|
||||
|
||||
req := mail.ListMessagesRequest{
|
||||
req := internalmail.ListMessagesRequest{
|
||||
Folder: folderVal,
|
||||
Page: pageVal,
|
||||
PageSize: pageSizeVal,
|
||||
Passphrase: session.AccessToken,
|
||||
Passphrase: session.MailPassphrase,
|
||||
Starred: starredPtr,
|
||||
Read: readPtr,
|
||||
Since: since,
|
||||
@@ -145,20 +158,16 @@ func mailReadCmd() *cobra.Command {
|
||||
return fmt.Errorf("failed to load config: %w", err)
|
||||
}
|
||||
|
||||
sessionMgr, err := auth.NewSessionManager()
|
||||
session, err := checkAuthenticated()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create session manager: %w", err)
|
||||
}
|
||||
session, err := sessionMgr.GetSession()
|
||||
if err != nil {
|
||||
return fmt.Errorf("not authenticated: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
client := api.NewProtonMailClient(cfg)
|
||||
client.SetAuthHeader(session.AccessToken)
|
||||
mailClient := mail.NewClient(client)
|
||||
mailClient := internalmail.NewClient(client)
|
||||
|
||||
msg, err := mailClient.GetMessage(messageID, session.AccessToken)
|
||||
msg, err := mailClient.GetMessage(messageID, session.MailPassphrase)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get message: %w", err)
|
||||
}
|
||||
@@ -198,7 +207,7 @@ func mailSendCmd() *cobra.Command {
|
||||
}
|
||||
|
||||
recipients := parseRecipients(to)
|
||||
var ccRecipients, bccRecipients []mail.Recipient
|
||||
var ccRecipients, bccRecipients []internalmail.Recipient
|
||||
if cc != "" {
|
||||
ccRecipients = parseRecipients(cc)
|
||||
}
|
||||
@@ -212,27 +221,23 @@ func mailSendCmd() *cobra.Command {
|
||||
return fmt.Errorf("failed to load config: %w", err)
|
||||
}
|
||||
|
||||
sessionMgr, err := auth.NewSessionManager()
|
||||
session, err := checkAuthenticated()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create session manager: %w", err)
|
||||
}
|
||||
session, err := sessionMgr.GetSession()
|
||||
if err != nil {
|
||||
return fmt.Errorf("not authenticated: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
client := api.NewProtonMailClient(cfg)
|
||||
client.SetAuthHeader(session.AccessToken)
|
||||
mailClient := mail.NewClient(client)
|
||||
mailClient := internalmail.NewClient(client)
|
||||
|
||||
req := mail.SendRequest{
|
||||
req := internalmail.SendRequest{
|
||||
To: recipients,
|
||||
CC: ccRecipients,
|
||||
BCC: bccRecipients,
|
||||
Subject: subject,
|
||||
Body: bodyContent,
|
||||
HTML: html,
|
||||
Passphrase: session.AccessToken,
|
||||
Passphrase: session.MailPassphrase,
|
||||
}
|
||||
|
||||
if err := mailClient.Send(req); err != nil {
|
||||
@@ -271,18 +276,14 @@ func mailDeleteCmd() *cobra.Command {
|
||||
return fmt.Errorf("failed to load config: %w", err)
|
||||
}
|
||||
|
||||
sessionMgr, err := auth.NewSessionManager()
|
||||
session, err := checkAuthenticated()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create session manager: %w", err)
|
||||
}
|
||||
session, err := sessionMgr.GetSession()
|
||||
if err != nil {
|
||||
return fmt.Errorf("not authenticated: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
client := api.NewProtonMailClient(cfg)
|
||||
client.SetAuthHeader(session.AccessToken)
|
||||
mailClient := mail.NewClient(client)
|
||||
mailClient := internalmail.NewClient(client)
|
||||
|
||||
if err := mailClient.PermanentlyDelete(messageID); err != nil {
|
||||
return fmt.Errorf("failed to delete message: %w", err)
|
||||
@@ -311,20 +312,16 @@ func mailTrashCmd() *cobra.Command {
|
||||
return fmt.Errorf("failed to load config: %w", err)
|
||||
}
|
||||
|
||||
sessionMgr, err := auth.NewSessionManager()
|
||||
session, err := checkAuthenticated()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create session manager: %w", err)
|
||||
}
|
||||
session, err := sessionMgr.GetSession()
|
||||
if err != nil {
|
||||
return fmt.Errorf("not authenticated: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
client := api.NewProtonMailClient(cfg)
|
||||
client.SetAuthHeader(session.AccessToken)
|
||||
mailClient := mail.NewClient(client)
|
||||
mailClient := internalmail.NewClient(client)
|
||||
|
||||
if err := mailClient.MoveToTrash(messageID, session.AccessToken); err != nil {
|
||||
if err := mailClient.MoveToTrash(messageID, session.MailPassphrase); err != nil {
|
||||
return fmt.Errorf("failed to move to trash: %w", err)
|
||||
}
|
||||
|
||||
@@ -336,7 +333,7 @@ func mailTrashCmd() *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
func printMessages(messages []mail.Message) error {
|
||||
func printMessages(messages []internalmail.Message) error {
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintln(w, "ID\tFrom\tSubject\tDate\tStarred\tRead")
|
||||
fmt.Fprintln(w, "--\t----\t-------\t----\t-------\t----")
|
||||
@@ -369,7 +366,7 @@ func printMessages(messages []mail.Message) error {
|
||||
return w.Flush()
|
||||
}
|
||||
|
||||
func printMessageDetail(msg *mail.Message) error {
|
||||
func printMessageDetail(msg *internalmail.Message) error {
|
||||
fmt.Printf("From: %s\n", msg.Sender.DisplayName())
|
||||
fmt.Printf("To: %s\n", formatRecipients(msg.Recipients))
|
||||
fmt.Printf("Subject: %s\n", msg.Subject)
|
||||
@@ -394,26 +391,30 @@ func printMessageDetail(msg *mail.Message) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseRecipients(input string) []mail.Recipient {
|
||||
var recipients []mail.Recipient
|
||||
func parseRecipients(input string) []internalmail.Recipient {
|
||||
var recipients []internalmail.Recipient
|
||||
for _, addr := range strings.Split(input, ",") {
|
||||
addr = strings.TrimSpace(addr)
|
||||
if addr == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
r := mail.Recipient{Address: addr}
|
||||
if strings.Contains(addr, "<") {
|
||||
parts := strings.SplitN(addr, "<", 2)
|
||||
r.Name = strings.TrimSpace(parts[0])
|
||||
r.Address = strings.Trim(parts[1], "<>")
|
||||
parsed, err := mail.ParseAddress(addr)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Warning: invalid address %q: %v\n", addr, err)
|
||||
continue
|
||||
}
|
||||
|
||||
r := internalmail.Recipient{
|
||||
Name: parsed.Name,
|
||||
Address: parsed.Address,
|
||||
}
|
||||
recipients = append(recipients, r)
|
||||
}
|
||||
return recipients
|
||||
}
|
||||
|
||||
func formatRecipients(recipients []mail.Recipient) string {
|
||||
func formatRecipients(recipients []internalmail.Recipient) string {
|
||||
parts := make([]string, len(recipients))
|
||||
for i, r := range recipients {
|
||||
parts[i] = r.DisplayName()
|
||||
@@ -455,18 +456,14 @@ func mailSearchCmd() *cobra.Command {
|
||||
return fmt.Errorf("failed to load config: %w", err)
|
||||
}
|
||||
|
||||
sessionMgr, err := auth.NewSessionManager()
|
||||
session, err := checkAuthenticated()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create session manager: %w", err)
|
||||
}
|
||||
session, err := sessionMgr.GetSession()
|
||||
if err != nil {
|
||||
return fmt.Errorf("not authenticated: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
client := api.NewProtonMailClient(cfg)
|
||||
client.SetAuthHeader(session.AccessToken)
|
||||
mailClient := mail.NewClient(client)
|
||||
mailClient := internalmail.NewClient(client)
|
||||
|
||||
pageVal, err := strconv.Atoi(page)
|
||||
if err != nil || pageVal < 1 {
|
||||
@@ -481,11 +478,11 @@ func mailSearchCmd() *cobra.Command {
|
||||
pageSizeVal = 100
|
||||
}
|
||||
|
||||
req := mail.SearchRequest{
|
||||
req := internalmail.SearchRequest{
|
||||
Query: searchQuery,
|
||||
Page: pageVal,
|
||||
PageSize: pageSizeVal,
|
||||
Passphrase: session.AccessToken,
|
||||
Passphrase: session.MailPassphrase,
|
||||
}
|
||||
|
||||
result, err := mailClient.SearchMessages(req)
|
||||
|
||||
Reference in New Issue
Block a user