package mail import ( "bytes" "encoding/json" "fmt" "io" "net/http" "net/url" "os" "time" "github.com/frenocorp/pop/internal/api" ) type Client struct { apiClient *api.ProtonMailClient baseURL string pgpService *PGPService } func NewClient(apiClient *api.ProtonMailClient) *Client { return &Client{ apiClient: apiClient, baseURL: apiClient.GetBaseURL(), } } func (c *Client) SetPGPService(svc *PGPService) { c.pgpService = svc } 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/mail/v4/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") httpReq.Header.Set("X-HTTP-Method-Override", "GET") 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) { var result struct { Message Message `json:"Message"` } reqURL := fmt.Sprintf("%s/mail/v4/messages/%s", c.baseURL, url.QueryEscape(messageID)) httpReq, err := http.NewRequest("GET", reqURL, nil) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } 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) } if err := json.Unmarshal(respBody, &result); err != nil { return nil, fmt.Errorf("failed to parse response: %w", err) } return &result.Message, nil } func (c *Client) Send(req SendRequest) error { payload := map[string]interface{}{ "Type": MessageTypeRegular, "Passphrase": req.Passphrase, "Subject": req.Subject, "HTML": req.HTML, "To": req.To, } if req.Body != "" { if c.pgpService != nil { encrypted, err := c.pgpService.EncryptBody(req.Body, req.Passphrase) if err != nil { return fmt.Errorf("failed to encrypt message body: %w", err) } payload["BodyEnc"] = encrypted } else { payload["Body"] = req.Body } } if len(req.CC) > 0 { payload["CC"] = req.CC } if len(req.BCC) > 0 { payload["BCC"] = req.BCC } jsonBody, err := json.Marshal(payload) if err != nil { return fmt.Errorf("failed to marshal request: %w", err) } reqURL := fmt.Sprintf("%s/mail/v4/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, passphrase string) error { body := map[string]string{ "Passphrase": passphrase, } jsonBody, err := json.Marshal(body) if err != nil { return fmt.Errorf("failed to marshal request: %w", err) } reqURL := fmt.Sprintf("%s/mail/v4/messages/%s/trash", c.baseURL, url.QueryEscape(messageID)) httpReq, err := http.NewRequest("PUT", 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 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/mail/v4/messages/%s", c.baseURL, url.QueryEscape(messageID)) httpReq, err := http.NewRequest("DELETE", 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": MessageTypeDraft, "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/mail/v4/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 { Message struct { MessageID string `json:"MessageID"` } `json:"Message"` } if err := json.Unmarshal(respBody, &result); err != nil { return "", fmt.Errorf("failed to parse response: %w", err) } return result.Message.MessageID, nil } func (c *Client) UpdateDraft(messageID string, draft Draft, passphrase string) error { body := map[string]interface{}{ "Message": map[string]interface{}{ "Passphrase": passphrase, "Subject": draft.Subject, "To": draft.To, "Body": draft.Body, }, } if len(draft.CC) > 0 { body["Message"].(map[string]interface{})["CC"] = draft.CC } jsonBody, err := json.Marshal(body) if err != nil { return fmt.Errorf("failed to marshal request: %w", err) } reqURL := fmt.Sprintf("%s/mail/v4/messages/%s", c.baseURL, url.QueryEscape(messageID)) httpReq, err := http.NewRequest("PUT", 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, passphrase string) error { body := map[string]string{ "Passphrase": passphrase, } jsonBody, err := json.Marshal(body) if err != nil { return fmt.Errorf("failed to marshal request: %w", err) } reqURL := fmt.Sprintf("%s/mail/v4/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 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/mail/v4/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 } func (c *Client) ListConversations(page int, pageSize int, passphrase string) (*ConversationResponse, error) { body := map[string]interface{}{ "Page": page, "PageSize": pageSize, "Passphrase": passphrase, } jsonBody, err := json.Marshal(body) if err != nil { return nil, fmt.Errorf("failed to marshal request: %w", err) } reqURL := fmt.Sprintf("%s/mail/v4/conversations", 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") httpReq.Header.Set("X-HTTP-Method-Override", "GET") resp, err := c.apiClient.Do(httpReq) if err != nil { return nil, fmt.Errorf("failed to list conversations: %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 ConversationResponse if err := json.Unmarshal(respBody, &result); err != nil { return nil, fmt.Errorf("failed to parse response: %w", err) } return &result, nil } func (c *Client) GetConversation(req GetConversationRequest) (*GetConversationResponse, error) { body := map[string]interface{}{ "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/mail/v4/conversations/%s", c.baseURL, url.QueryEscape(req.ConversationID)) 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") httpReq.Header.Set("X-HTTP-Method-Override", "GET") resp, err := c.apiClient.Do(httpReq) if err != nil { return nil, fmt.Errorf("failed to get conversation: %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 GetConversationResponse if err := json.Unmarshal(respBody, &result); err != nil { return nil, fmt.Errorf("failed to parse response: %w", err) } return &result, nil } func (c *Client) BulkDelete(messageIDs []string) (*BulkResponse, error) { body := map[string]interface{}{ "MessageIDs": messageIDs, } jsonBody, err := json.Marshal(body) if err != nil { return nil, fmt.Errorf("failed to marshal request: %w", err) } reqURL := fmt.Sprintf("%s/mail/v4/messages/bulk", 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") httpReq.Header.Set("X-HTTP-Method-Override", "DELETE") resp, err := c.apiClient.Do(httpReq) if err != nil { return nil, fmt.Errorf("failed to bulk delete: %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 BulkResponse if err := json.Unmarshal(respBody, &result); err != nil { return nil, fmt.Errorf("failed to parse response: %w", err) } return &result, nil } func (c *Client) BulkTrash(messageIDs []string, passphrase string) (*BulkResponse, error) { body := map[string]interface{}{ "MessageIDs": messageIDs, "Passphrase": passphrase, } jsonBody, err := json.Marshal(body) if err != nil { return nil, fmt.Errorf("failed to marshal request: %w", err) } reqURL := fmt.Sprintf("%s/mail/v4/messages/bulk/trash", 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 bulk trash: %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 BulkResponse if err := json.Unmarshal(respBody, &result); err != nil { return nil, fmt.Errorf("failed to parse response: %w", err) } return &result, nil } func (c *Client) BulkStar(messageIDs []string, starred bool) (*BulkResponse, error) { body := map[string]interface{}{ "MessageIDs": messageIDs, "Starred": starred, } jsonBody, err := json.Marshal(body) if err != nil { return nil, fmt.Errorf("failed to marshal request: %w", err) } reqURL := fmt.Sprintf("%s/mail/v4/messages/bulk/star", 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 bulk star: %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 BulkResponse if err := json.Unmarshal(respBody, &result); err != nil { return nil, fmt.Errorf("failed to parse response: %w", err) } return &result, nil } func (c *Client) BulkMarkRead(messageIDs []string, read bool) (*BulkResponse, error) { body := map[string]interface{}{ "MessageIDs": messageIDs, "Read": read, } jsonBody, err := json.Marshal(body) if err != nil { return nil, fmt.Errorf("failed to marshal request: %w", err) } reqURL := fmt.Sprintf("%s/mail/v4/messages/bulk/read", 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 bulk mark read: %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 BulkResponse if err := json.Unmarshal(respBody, &result); err != nil { return nil, fmt.Errorf("failed to parse response: %w", err) } return &result, nil } func (c *Client) ExportMessages(req ExportRequest) ([]ExportedMessage, error) { var messages []Message if len(req.MessageIDs) > 0 { for _, id := range req.MessageIDs { msg, err := c.GetMessage(id, req.Passphrase) if err != nil { return nil, fmt.Errorf("failed to get message %s: %w", id, err) } messages = append(messages, *msg) } } else if req.Search != "" { searchReq := SearchRequest{ Query: req.Search, Page: 1, PageSize: 100, Passphrase: req.Passphrase, } searchResult, err := c.SearchMessages(searchReq) if err != nil { return nil, fmt.Errorf("failed to search messages: %w", err) } messages = searchResult.Messages } else { listReq := ListMessagesRequest{ Folder: req.Folder, Page: 1, PageSize: 100, Passphrase: req.Passphrase, } if req.Since > 0 { listReq.Since = req.Since } listResult, err := c.ListMessages(listReq) if err != nil { return nil, fmt.Errorf("failed to list messages: %w", err) } messages = listResult.Messages } exported := make([]ExportedMessage, 0, len(messages)) for _, msg := range messages { exp := ExportedMessage{ MessageID: msg.MessageID, ConversationID: msg.ConversationID, From: msg.Sender, To: msg.Recipients, Subject: msg.Subject, Body: msg.Body, Date: msg.CreatedAt.Format(time.RFC3339), Starred: msg.Starred, Read: msg.Read, Attachments: msg.Attachments, } exported = append(exported, exp) } return exported, nil } func (c *Client) ImportMessages(req ImportRequest) (*ImportResponse, error) { fileData, err := os.ReadFile(req.FilePath) if err != nil { return nil, fmt.Errorf("failed to read import file: %w", err) } var messages []ExportedMessage if req.Format == ExportFormatJSON { if err := json.Unmarshal(fileData, &messages); err != nil { return nil, fmt.Errorf("failed to parse import file: %w", err) } } else { return nil, fmt.Errorf("unsupported import format: %s (use json)", req.Format.String()) } if len(messages) == 0 { return &ImportResponse{Total: 0, ImportedCount: 0}, nil } imported := 0 var errors []BulkError for _, msg := range messages { sendReq := SendRequest{ To: []Recipient{msg.From.ToRecipient()}, Subject: msg.Subject, Body: msg.Body, HTML: msg.HTML, Passphrase: req.Passphrase, } if err := c.Send(sendReq); err != nil { errors = append(errors, BulkError{ MessageID: msg.MessageID, Error: err.Error(), }) continue } imported++ } return &ImportResponse{ ImportedCount: imported, Total: len(messages), Errors: errors, }, nil }