Files
pop/internal/mail/client.go
Paperclip af25fd5575 FRE-682: Add folder/label management, search, and fix PGP build
- Add pop mail search CLI command with pagination support
- Create internal/labels package with types and API client
- Add folder list/create/update/delete CLI commands
- Add label list/create/update/delete/apply/remove CLI commands
- Register folder and label commands in root.go
- Fix gopenpgp v2 API mismatches in pgp.go (NewPlainMessage, Armor,
  KeyRing.Encrypt/Decrypt, SessionKey)
- Fix NewSessionManager error handling across cmd files
- Fix variable shadowing bug in mail/client.go
2026-04-28 06:37:47 -04:00

367 lines
9.2 KiB
Go

package mail
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"github.com/frenocorp/pop/internal/api"
)
type Client struct {
apiClient *api.ProtonMailClient
baseURL string
}
func NewClient(apiClient *api.ProtonMailClient) *Client {
return &Client{
apiClient: apiClient,
baseURL: apiClient.GetBaseURL(),
}
}
func (c *Client) ListMessages(req ListMessagesRequest) (*ListMessagesResponse, error) {
body := map[string]interface{}{
"Page": req.Page,
"PageSize": req.PageSize,
"Passphrase": req.Passphrase,
}
if req.Folder != FolderInbox {
body["Type"] = int(req.Folder)
}
if req.Starred != nil {
body["Starred"] = *req.Starred
}
if req.Read != nil {
body["Read"] = *req.Read
}
if req.Since > 0 {
body["Since"] = req.Since
}
jsonBody, err := json.Marshal(body)
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %w", err)
}
reqURL := fmt.Sprintf("%s/api/messages", c.baseURL)
httpReq, err := http.NewRequest("POST", reqURL, bytes.NewBuffer(jsonBody))
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
httpReq.Header.Set("Content-Type", "application/json")
resp, err := c.apiClient.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("failed to list messages: %w", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
var result ListMessagesResponse
if err := json.Unmarshal(respBody, &result); err != nil {
return nil, fmt.Errorf("failed to parse response: %w", err)
}
return &result, nil
}
func (c *Client) GetMessage(messageID string, passphrase string) (*Message, error) {
body := map[string]string{
"Passphrase": passphrase,
}
jsonBody, err := json.Marshal(body)
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %w", err)
}
reqURL := fmt.Sprintf("%s/api/messages/%s", c.baseURL, url.QueryEscape(messageID))
httpReq, err := http.NewRequest("POST", reqURL, bytes.NewBuffer(jsonBody))
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
httpReq.Header.Set("Content-Type", "application/json")
resp, err := c.apiClient.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("failed to get message: %w", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
var result struct {
Data Message `json:"Data"`
}
if err := json.Unmarshal(respBody, &result); err != nil {
return nil, fmt.Errorf("failed to parse response: %w", err)
}
return &result.Data, nil
}
func (c *Client) Send(req SendRequest) error {
body := map[string]interface{}{
"Type": "0",
"Passphrase": req.Passphrase,
"Subject": req.Subject,
"HTML": req.HTML,
"To": req.To,
"Body": req.Body,
}
if len(req.CC) > 0 {
body["CC"] = req.CC
}
if len(req.BCC) > 0 {
body["BCC"] = req.BCC
}
jsonBody, err := json.Marshal(body)
if err != nil {
return fmt.Errorf("failed to marshal request: %w", err)
}
reqURL := fmt.Sprintf("%s/api/messages", c.baseURL)
httpReq, err := http.NewRequest("POST", reqURL, bytes.NewBuffer(jsonBody))
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
httpReq.Header.Set("Content-Type", "application/json")
resp, err := c.apiClient.Do(httpReq)
if err != nil {
return fmt.Errorf("failed to send message: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("send failed (status %d): %s", resp.StatusCode, string(body))
}
return nil
}
func (c *Client) MoveToTrash(messageID string) error {
formData := url.Values{}
reqURL := fmt.Sprintf("%s/api/messages/%s/movetotrash", c.baseURL, url.QueryEscape(messageID))
httpReq, err := http.NewRequest("POST", reqURL, bytes.NewBufferString(formData.Encode()))
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
httpReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := c.apiClient.Do(httpReq)
if err != nil {
return fmt.Errorf("failed to move to trash: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("trash failed (status %d): %s", resp.StatusCode, string(body))
}
return nil
}
func (c *Client) PermanentlyDelete(messageID string) error {
reqURL := fmt.Sprintf("%s/api/messages/%s/delete", c.baseURL, url.QueryEscape(messageID))
httpReq, err := http.NewRequest("POST", reqURL, nil)
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
resp, err := c.apiClient.Do(httpReq)
if err != nil {
return fmt.Errorf("failed to delete message: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("delete failed (status %d): %s", resp.StatusCode, string(body))
}
return nil
}
func (c *Client) SaveDraft(draft Draft, passphrase string) (string, error) {
body := map[string]interface{}{
"Type": "2",
"Passphrase": passphrase,
"Subject": draft.Subject,
"To": draft.To,
"Body": draft.Body,
}
if len(draft.CC) > 0 {
body["CC"] = draft.CC
}
if len(draft.BCC) > 0 {
body["BCC"] = draft.BCC
}
jsonBody, err := json.Marshal(body)
if err != nil {
return "", fmt.Errorf("failed to marshal request: %w", err)
}
reqURL := fmt.Sprintf("%s/api/messages", c.baseURL)
httpReq, err := http.NewRequest("POST", reqURL, bytes.NewBuffer(jsonBody))
if err != nil {
return "", fmt.Errorf("failed to create request: %w", err)
}
httpReq.Header.Set("Content-Type", "application/json")
resp, err := c.apiClient.Do(httpReq)
if err != nil {
return "", fmt.Errorf("failed to save draft: %w", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("failed to read response: %w", err)
}
var result struct {
Data struct {
MessageID string `json:"MessageID"`
} `json:"Data"`
}
if err := json.Unmarshal(respBody, &result); err != nil {
return "", fmt.Errorf("failed to parse response: %w", err)
}
return result.Data.MessageID, nil
}
func (c *Client) UpdateDraft(messageID string, draft Draft, passphrase string) error {
body := map[string]interface{}{
"Passphrase": passphrase,
"Subject": draft.Subject,
"To": draft.To,
"Body": draft.Body,
}
if len(draft.CC) > 0 {
body["CC"] = draft.CC
}
jsonBody, err := json.Marshal(body)
if err != nil {
return fmt.Errorf("failed to marshal request: %w", err)
}
reqURL := fmt.Sprintf("%s/api/messages/%s", c.baseURL, url.QueryEscape(messageID))
httpReq, err := http.NewRequest("POST", reqURL, bytes.NewBuffer(jsonBody))
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
httpReq.Header.Set("Content-Type", "application/json")
resp, err := c.apiClient.Do(httpReq)
if err != nil {
return fmt.Errorf("failed to update draft: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("update failed (status %d): %s", resp.StatusCode, string(body))
}
return nil
}
func (c *Client) SendDraft(messageID string) error {
formData := url.Values{}
reqURL := fmt.Sprintf("%s/api/messages/%s/send", c.baseURL, url.QueryEscape(messageID))
httpReq, err := http.NewRequest("POST", reqURL, bytes.NewBufferString(formData.Encode()))
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
httpReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := c.apiClient.Do(httpReq)
if err != nil {
return fmt.Errorf("failed to send draft: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("send draft failed (status %d): %s", resp.StatusCode, string(body))
}
return nil
}
func (c *Client) ListDrafts(page int, pageSize int, passphrase string) (*ListMessagesResponse, error) {
req := ListMessagesRequest{
Folder: FolderDraft,
Page: page,
PageSize: pageSize,
Passphrase: passphrase,
}
return c.ListMessages(req)
}
func (c *Client) SearchMessages(req SearchRequest) (*SearchResponse, error) {
body := map[string]interface{}{
"Query": req.Query,
"Page": req.Page,
"PageSize": req.PageSize,
"Passphrase": req.Passphrase,
}
jsonBody, err := json.Marshal(body)
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %w", err)
}
reqURL := fmt.Sprintf("%s/api/messages/search", c.baseURL)
httpReq, err := http.NewRequest("POST", reqURL, bytes.NewBuffer(jsonBody))
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
httpReq.Header.Set("Content-Type", "application/json")
resp, err := c.apiClient.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("failed to search messages: %w", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
var result SearchResponse
if err := json.Unmarshal(respBody, &result); err != nil {
return nil, fmt.Errorf("failed to parse response: %w", err)
}
return &result, nil
}