diff --git a/internal/ai/ai.go b/internal/ai/ai.go
index 664b8d7..c1a02f0 100644
--- a/internal/ai/ai.go
+++ b/internal/ai/ai.go
@@ -48,52 +48,63 @@ func (a *AI) GenerateReply(emailContent string, contextContent map[string]string
}
// Prepare the system message and user message
- systemMsg := "You are a helpful assistant. Use the provided context to help answer the email professionally and accurately."
- userMsg := fmt.Sprintf("Using the following context:\n%s\n\nPlease generate a reply for this email:\n%s", context, emailContent)
+ systemMsg := "You are a helpful assistant responding to emails. Analyze the language of the incoming email and translate the response to the same language. Format your response in HTML. Do not include any explanations or extra text - just write the email response directly in HTML format. Use appropriate HTML tags for formatting."
+ userMsg := fmt.Sprintf("Using the following context:\n%s\n\nPlease analyze the language of and generate a reply in the same language for this email:\n%s", context, emailContent)
messages := []Message{
{Role: "system", Content: systemMsg},
{Role: "user", Content: userMsg},
}
- reqBody := OpenRouterRequest{
- Model: a.config.Model,
- Messages: messages,
- Temperature: a.config.Temperature,
- MaxTokens: a.config.MaxTokens,
+ const maxRetries = 3
+ var lastErr error
+
+ for attempt := 0; attempt < maxRetries; attempt++ {
+ reqBody := OpenRouterRequest{
+ Model: a.config.Model,
+ Messages: messages,
+ Temperature: a.config.Temperature,
+ MaxTokens: a.config.MaxTokens,
+ }
+
+ jsonData, err := json.Marshal(reqBody)
+ if err != nil {
+ return "", err
+ }
+
+ req, err := http.NewRequest("POST", "https://openrouter.ai/api/v1/chat/completions", bytes.NewBuffer(jsonData))
+ if err != nil {
+ return "", err
+ }
+
+ req.Header.Set("Content-Type", "application/json")
+ req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", a.config.OpenRouterAPIKey))
+
+ resp, err := a.client.Do(req)
+ if err != nil {
+ lastErr = err
+ continue
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ lastErr = fmt.Errorf("OpenRouter API returned status code: %d", resp.StatusCode)
+ continue
+ }
+
+ var result OpenRouterResponse
+ if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
+ lastErr = err
+ continue
+ }
+
+ if len(result.Choices) == 0 || result.Choices[0].Message.Content == "" {
+ lastErr = fmt.Errorf("empty response received")
+ continue
+ }
+
+ return result.Choices[0].Message.Content, nil
}
- jsonData, err := json.Marshal(reqBody)
- if err != nil {
- return "", err
- }
-
- req, err := http.NewRequest("POST", "https://openrouter.ai/api/v1/chat/completions", bytes.NewBuffer(jsonData))
- if err != nil {
- return "", err
- }
-
- req.Header.Set("Content-Type", "application/json")
- req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", a.config.OpenRouterAPIKey))
-
- resp, err := a.client.Do(req)
- if err != nil {
- return "", err
- }
- defer resp.Body.Close()
-
- if resp.StatusCode != http.StatusOK {
- return "", fmt.Errorf("OpenRouter API returned status code: %d", resp.StatusCode)
- }
-
- var result OpenRouterResponse
- if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
- return "", err
- }
-
- if len(result.Choices) == 0 {
- return "", fmt.Errorf("no response generated")
- }
-
- return result.Choices[0].Message.Content, nil
+ return "", fmt.Errorf("failed after %d attempts, last error: %v", maxRetries, lastErr)
}
diff --git a/internal/imap/imap.go b/internal/imap/imap.go
index 8d1b2e6..6b000b2 100644
--- a/internal/imap/imap.go
+++ b/internal/imap/imap.go
@@ -1,8 +1,12 @@
package imap
import (
+ "bytes"
"fmt"
"io"
+ "mime"
+ "mime/multipart"
+ "net/mail"
"paraclub-ai-mailer/config"
"strings"
"time"
@@ -20,10 +24,19 @@ type IMAPClient struct {
// MessageLiteral implements imap.Literal for draft messages
type MessageLiteral struct {
content []byte
+ pos int
}
func (m *MessageLiteral) Read(p []byte) (n int, err error) {
- return copy(p, m.content), io.EOF
+ if m.pos >= len(m.content) {
+ return 0, io.EOF
+ }
+ n = copy(p, m.content[m.pos:])
+ m.pos += n
+ if m.pos >= len(m.content) {
+ err = io.EOF
+ }
+ return
}
func (m *MessageLiteral) Len() int {
@@ -63,10 +76,63 @@ type Email struct {
Body string
}
-func (ic *IMAPClient) FetchUnprocessedEmails() ([]Email, error) {
- _, err := ic.client.Select(ic.config.MailboxIn, false)
+func (ic *IMAPClient) ensureConnection() error {
+ if ic.client == nil {
+ return ic.reconnect()
+ }
+
+ // Try to check connection by NOOPing
+ if err := ic.client.Noop(); err != nil {
+ return ic.reconnect()
+ }
+ return nil
+}
+
+func (ic *IMAPClient) reconnect() error {
+ addr := fmt.Sprintf("%s:%d", ic.config.Server, ic.config.Port)
+ var c *client.Client
+ var err error
+
+ if ic.config.UseTLS {
+ c, err = client.DialTLS(addr, nil)
+ } else {
+ c, err = client.Dial(addr)
+ }
if err != nil {
- return nil, err
+ return fmt.Errorf("failed to connect to IMAP server: %v", err)
+ }
+
+ if err := c.Login(ic.config.Username, ic.config.Password); err != nil {
+ c.Close()
+ return fmt.Errorf("failed to login: %v", err)
+ }
+
+ if ic.client != nil {
+ ic.client.Close()
+ }
+ ic.client = c
+ return nil
+}
+
+func (ic *IMAPClient) FetchUnprocessedEmails() ([]Email, error) {
+ if err := ic.ensureConnection(); err != nil {
+ return nil, fmt.Errorf("failed to ensure IMAP connection: %v", err)
+ }
+
+ // Make sure we have a valid mailbox name
+ if ic.config.MailboxIn == "" {
+ return nil, fmt.Errorf("mailbox_in not configured")
+ }
+
+ // Select mailbox and get status
+ mbox, err := ic.client.Select(ic.config.MailboxIn, false)
+ if err != nil {
+ return nil, fmt.Errorf("failed to select mailbox %s: %v", ic.config.MailboxIn, err)
+ }
+
+ // If mailbox is empty, return early
+ if mbox.Messages == 0 {
+ return nil, nil
}
criteria := imap.NewSearchCriteria()
@@ -74,7 +140,7 @@ func (ic *IMAPClient) FetchUnprocessedEmails() ([]Email, error) {
uids, err := ic.client.Search(criteria)
if err != nil {
- return nil, err
+ return nil, fmt.Errorf("search failed: %v", err)
}
if len(uids) == 0 {
@@ -84,35 +150,54 @@ func (ic *IMAPClient) FetchUnprocessedEmails() ([]Email, error) {
seqSet := new(imap.SeqSet)
seqSet.AddNum(uids...)
- messages := make(chan *imap.Message, 10)
+ // Fetch both envelope and body
section := &imap.BodySectionName{Peek: true}
+ items := []imap.FetchItem{imap.FetchEnvelope, section.FetchItem()}
+ messages := make(chan *imap.Message, 10)
done := make(chan error, 1)
+
go func() {
- done <- ic.client.Fetch(seqSet, []imap.FetchItem{section.FetchItem()}, messages)
+ done <- ic.client.Fetch(seqSet, items, messages)
}()
var emails []Email
for msg := range messages {
+ // Skip if message doesn't have an envelope
+ if msg.Envelope == nil {
+ continue
+ }
+
+ // Skip if already processed
if ic.processed[msg.Envelope.MessageId] {
continue
}
+ // Skip if no message ID
+ if msg.Envelope.MessageId == "" {
+ continue
+ }
+
r := msg.GetBody(section)
if r == nil {
continue
}
var bodyBuilder strings.Builder
- _, err := io.Copy(&bodyBuilder, r)
- if err != nil {
+ if _, err := io.Copy(&bodyBuilder, r); err != nil {
continue
}
+ // Make sure we have a valid from address
+ var from string
+ if len(msg.Envelope.From) > 0 {
+ from = msg.Envelope.From[0].Address()
+ }
+
emails = append(emails, Email{
ID: msg.Envelope.MessageId,
Subject: msg.Envelope.Subject,
- From: msg.Envelope.From[0].Address(),
+ From: from,
Body: bodyBuilder.String(),
})
@@ -120,42 +205,137 @@ func (ic *IMAPClient) FetchUnprocessedEmails() ([]Email, error) {
}
if err := <-done; err != nil {
- return nil, err
+ return nil, fmt.Errorf("fetch failed: %v", err)
}
return emails, nil
}
func (ic *IMAPClient) SaveDraft(email Email, response string) error {
- _, err := ic.client.Select(ic.config.DraftBox, false)
- if err != nil {
- return err
+ if err := ic.ensureConnection(); err != nil {
+ return fmt.Errorf("failed to ensure IMAP connection: %v", err)
}
- // Format the draft message
+ _, err := ic.client.Select(ic.config.DraftBox, false)
+ if err != nil {
+ return fmt.Errorf("failed to select draft box: %v", err)
+ }
+
+ // Format the draft message with HTML response and original email headers + content
draft := fmt.Sprintf("From: %s\r\n"+
"To: %s\r\n"+
"Subject: Re: %s\r\n"+
- "Content-Type: text/plain; charset=UTF-8\r\n"+
- "\r\n%s",
+ "MIME-Version: 1.0\r\n"+
+ "Content-Type: text/html; charset=UTF-8\r\n"+
+ "\r\n"+
+ "%s\r\n"+
+ "
\r\n"+
+ "