diff --git a/internal/ai/ai.go b/internal/ai/ai.go
index f20d767..6f1e38f 100644
--- a/internal/ai/ai.go
+++ b/internal/ai/ai.go
@@ -43,12 +43,40 @@ func New(cfg config.AIConfig) *AI {
}
}
+func (a *AI) detectLanguage(emailContent string) (string, error) {
+ logger.WithField("emailContentLength", len(emailContent)).Debug("Starting language detection")
+
+ systemMsg := "You are a language detection assistant. Analyze the provided text and respond ONLY with the language name (e.g., 'English', 'German', 'French', etc.). No other text or explanation."
+ userMsg := fmt.Sprintf("Detect the language of this text:\n\n%s", emailContent)
+
+ messages := []Message{
+ {Role: "system", Content: systemMsg},
+ {Role: "user", Content: userMsg},
+ }
+
+ response, err := a.makeAPIRequest(messages)
+ if err != nil {
+ return "", fmt.Errorf("language detection failed: %v", err)
+ }
+
+ // The response should be just the language code
+ langCode := response
+ logger.WithField("detectedLanguage", langCode).Debug("Language detection completed")
+ return langCode, nil
+}
+
func (a *AI) GenerateReply(emailContent string, contextContent map[string]string) (string, error) {
logger.WithFields(logrus.Fields{
"emailContentLength": len(emailContent),
- "contextUrls": len(contextContent),
+ "contextUrls": len(contextContent),
}).Debug("Starting AI reply generation")
+ // First, detect the language
+ lang, err := a.detectLanguage(emailContent)
+ if err != nil {
+ return "", err
+ }
+
// Prepare context from all URLs
var context string
for url, content := range contextContent {
@@ -59,19 +87,22 @@ func (a *AI) GenerateReply(emailContent string, contextContent map[string]string
}).Debug("Added context from URL")
}
- // Prepare the system message and user message
- // 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."
- // systemMsg := "You are a helpful assistant who responds to emails. For each incoming email, first detect its language and then generate your response in the exact same language. Your reply must be written directly in HTML format using appropriate HTML tags for structure and styling. Do not include any explanations, commentary, or extra text—simply output the email response in HTML."
- // systemMsg := "You are a helpful assistant who responds to emails. For each incoming email, first detect its language and then generate your response in the exact same language. Your reply must be written directly in HTML format using appropriate HTML tags for structure and styling. Do not include a subject line, explanations, commentary, or any extra text—simply output the email response content in HTML."
- // systemMsg := "You are a helpful assistant who responds to emails. For every incoming email, carefully detect and confirm its language. Then, generate your email response entirely in that same language without deviation. Format your reply solely in HTML using appropriate HTML tags for structure and styling, and do not include a subject line, explanations, or any extra text. Ensure that every part of your response exactly matches the language of the incoming email."
- systemMsg := "You are a helpful assistant who responds to emails. For every incoming email, strictly use only the email's content to detect its language and ignore any external or additional context. Then, generate your email response entirely in that same language without deviation. Format your reply solely in HTML using appropriate HTML tags for structure and styling, and do not include a subject line, explanations, commentary, or any extra text. Ensure that every part of your response exactly matches the language of the incoming email."
- 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)
+ // Prepare the system message with language-specific instruction
+ systemMsg := fmt.Sprintf("You are a helpful assistant who responds to emails.Regardless of the language used in the email content or context, your response must be entirely in %s. Format your reply solely in HTML using appropriate HTML tags for structure and styling. Do not include a subject line, explanations, commentary, or any extra text.", lang)
+ logger.WithFields(logrus.Fields{
+ "systemprompt": systemMsg,
+ }).Debug("Generating system prompt")
+ userMsg := fmt.Sprintf("### Additional Context:\n%s\n\n### Email Body:\n%s", context, emailContent)
messages := []Message{
{Role: "system", Content: systemMsg},
{Role: "user", Content: userMsg},
}
+ return a.makeAPIRequest(messages)
+}
+
+func (a *AI) makeAPIRequest(messages []Message) (string, error) {
const maxRetries = 3
var lastErr error
diff --git a/internal/imap/imap.go b/internal/imap/imap.go
index 2d6ba8a..53be800 100644
--- a/internal/imap/imap.go
+++ b/internal/imap/imap.go
@@ -268,7 +268,6 @@ func (ic *IMAPClient) SaveDraft(email Email, response string) error {
// by removing email headers and MIME boundaries
func extractMessageContent(body string) string {
logger.WithField("bodyLength", len(body)).Debug("Starting message content extraction")
-
msg, err := mail.ReadMessage(strings.NewReader(body))
if err != nil {
logger.WithFields(logrus.Fields{
@@ -295,89 +294,106 @@ func extractMessageContent(body string) string {
"params": params,
}).Debug("Parsed message Content-Type")
+ var content string
if strings.HasPrefix(mediaType, "multipart/") {
- boundary := params["boundary"]
- if boundary == "" {
- logger.WithField("mediaType", mediaType).Debug("No boundary found in multipart message, falling back to simple extraction")
- return fallbackExtractContent(body)
- }
-
- logger.WithFields(logrus.Fields{
- "mediaType": mediaType,
- "boundary": boundary,
- }).Debug("Processing multipart message")
-
- reader := multipart.NewReader(msg.Body, boundary)
- var textContent string
- partIndex := 0
-
- for {
- part, err := reader.NextPart()
- if err == io.EOF {
- logger.Debug("Finished processing all multipart parts")
- break
- }
- if err != nil {
- logger.WithFields(logrus.Fields{
- "error": err,
- "partIndex": partIndex,
- }).Debug("Error reading multipart part, falling back to simple extraction")
- return fallbackExtractContent(body)
- }
-
- contentType := part.Header.Get("Content-Type")
- logger.WithFields(logrus.Fields{
- "partIndex": partIndex,
- "contentType": contentType,
- }).Debug("Processing message part")
-
- if strings.HasPrefix(contentType, "text/plain") {
- buf := new(bytes.Buffer)
- _, err := buf.ReadFrom(part)
- if err != nil {
- logger.WithFields(logrus.Fields{
- "error": err,
- "partIndex": partIndex,
- }).Debug("Failed to read text/plain part")
- continue
- }
- textContent = buf.String()
- logger.WithFields(logrus.Fields{
- "partIndex": partIndex,
- "contentLength": len(textContent),
- }).Debug("Successfully extracted text/plain content")
- textContent = strings.ReplaceAll(textContent, "\n", "
\n")
- textContent = strings.ReplaceAll(textContent, "\r\n", "
\n")
- break
- }
- partIndex++
- }
-
- if textContent != "" {
- logger.WithField("contentLength", len(textContent)).Debug("Successfully extracted content from multipart message")
- return textContent
- }
- logger.Debug("No text/plain content found in multipart message, trying to read body directly")
+ content = handleMultipartMessage(msg.Body, params["boundary"])
+ } else {
+ content = handleSinglePartMessage(msg.Body)
}
- buf := new(bytes.Buffer)
- _, err = buf.ReadFrom(msg.Body)
- if err != nil {
- logger.WithFields(logrus.Fields{
- "error": err,
- "mediaType": mediaType,
- }).Debug("Failed to read message body, falling back to simple extraction")
+ if content == "" {
+ logger.Debug("No content extracted, falling back to simple extraction")
return fallbackExtractContent(body)
}
- content := buf.String()
+ // Clean up the content
+ content = cleanMessageContent(content)
+
+ logger.WithField("contentLength", len(content)).Debug("Successfully extracted and cleaned message content")
+ return content
+}
+
+func handleMultipartMessage(reader io.Reader, boundary string) string {
+ if boundary == "" {
+ logger.Debug("No boundary found in multipart message")
+ return ""
+ }
+
+ mReader := multipart.NewReader(reader, boundary)
+ var textContent string
+ partIndex := 0
+
+ for {
+ part, err := mReader.NextPart()
+ if err == io.EOF {
+ break
+ }
+ if err != nil {
+ logger.WithError(err).Debug("Error reading multipart part")
+ return ""
+ }
+
+ contentType := part.Header.Get("Content-Type")
+ if strings.HasPrefix(contentType, "text/plain") {
+ buf := new(bytes.Buffer)
+ if _, err := buf.ReadFrom(part); err != nil {
+ continue
+ }
+ textContent = buf.String()
+ break
+ }
+ partIndex++
+ }
+
+ return textContent
+}
+
+func handleSinglePartMessage(reader io.Reader) string {
+ buf := new(bytes.Buffer)
+ if _, err := buf.ReadFrom(reader); err != nil {
+ logger.WithError(err).Debug("Failed to read message body")
+ return ""
+ }
+ return buf.String()
+}
+
+func cleanMessageContent(content string) string {
+ // Remove any remaining email headers that might be in the body
+ lines := strings.Split(content, "\n")
+ var cleanLines []string
+ headerSection := true
+
+ for _, line := range lines {
+ trimmed := strings.TrimSpace(line)
+
+ // Empty line marks the end of headers
+ if headerSection && trimmed == "" {
+ headerSection = false
+ continue
+ }
+
+ // Skip header lines
+ if headerSection && (strings.Contains(trimmed, ":") || trimmed == "") {
+ continue
+ }
+
+ // Add non-header lines
+ if !headerSection {
+ cleanLines = append(cleanLines, line)
+ }
+ }
+
+ content = strings.Join(cleanLines, "\n")
+
+ // Convert newlines to HTML breaks for display
content = strings.ReplaceAll(content, "\r\n", "
\n")
content = strings.ReplaceAll(content, "\n", "
\n")
- logger.WithFields(logrus.Fields{
- "contentLength": len(content),
- "mediaType": mediaType,
- }).Debug("Successfully extracted content from message body")
- return content
+
+ // Remove any remaining email signature markers
+ content = strings.Split(content, "\n-- ")[0]
+ content = strings.Split(content, "
-- ")[0]
+
+ return strings.TrimSpace(content)
}
// fallbackExtractContent is the previous implementation used as fallback