使用Golang构建一个支持IMAP同步的💌邮件编写工具

使用Golang构建一个支持IMAP同步的💌邮件编写工具 我构建了一个支持IMAP同步的💌电子邮件撰写工具。

该工具通过AI分析您的收件箱邮件,自动生成专业性的肯定和否定邮件回复,让您可以在通过常规邮件客户端发送前进行选择和编辑。

大家好,

我构建了一个AI驱动的邮件撰写工具,它通过IMAP与您的电子邮件收件箱集成。该系统:

  • 读取您收件箱中的新邮件
  • 使用LLM(大语言模型)自动撰写两种回复选项:
    • 专业的肯定回复
    • 专业的否定回复
  • 您可以在发送前编辑任一回复。邮件发送后,会通过IMAP同步到您邮箱的“已发送”文件夹,确保它能出现在您常用的邮件客户端中(无论您使用网页邮箱、Thunderbird还是其他邮件工具)。

我使用Go4lage作为技术栈。https://go4lage.com/

在开发过程中,我使用Gemini作为LLM API,然后在生产环境中切换到了Mistral。(如果需要,我可以热切换到任何合适的LLM API,包括自托管的方案。)😉

注意:有一个演示视频,但请注意它是德语的。🇩🇪

proof of concept KI-Mail (KI Mailbeantworter)


更多关于使用Golang构建一个支持IMAP同步的💌邮件编写工具的实战教程也可以访问 https://www.itying.com/category-94-b0.html

8 回复

你是使用Wails构建桌面应用程序吗?还是使用了其他技术?

更多关于使用Golang构建一个支持IMAP同步的💌邮件编写工具的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


我一个字都没听懂,但我看了演示,明白了要点! 😊 你说的“热插拔”是指LLM在应用程序中是可配置的吗?还是说你必须重新编译你的二进制文件?

如果实现了热交换,就无需重新编译。

一般来说,只要你不使用特定的功能,就可以更换所有的LLM API。坦率地说,我从未需要过任何特定功能,它们给人的感觉就像是专门用来将经验不足的开发者锁定在特定供应商那里的。确实需要做一些适配工作,比如编写特定版本的提示词,或者如果需要的话,更换分词器,但这并不算太糟糕。

我又开发了一款新的应用程序。这次是桌面端的,主要用于翻译和文本修正。是的,它支持我所知的所有LLM API。当然,它也兼容DeepSeek,尽管有些人总是表现得好像这有什么特别似的。🙄

(这个应用可以通过ollama在本地运行。qwen2.5:14b模型的性能还算可以。)

ktext-quote

therecipe/qt

GitHub - therecipe/qt: 用于 Go (Golang) 的 Qt 绑定,支持…

用于 Go (Golang) 的 Qt 绑定,支持 Windows / macOS / Linux / FreeBSD / Android / iOS / Sailfish OS / Raspberry Pi / AsteroidOS / Ubuntu Touch / JavaScript / WebAssembly

我使用了这个,我不是很满意,因为它需要 2-3 秒才能启动。但也许是我用错了。

我刚刚向 kde.org 申请了一位导师,希望我们能在下一个 KDE 版本中将其包含在 kmail 中并作为 ktext。

从 Gemini 转换到 Mistral 对性能或响应质量有何影响?

性能并不是那么重要,因为调用是在用户甚至还没检查应用之前就从服务器发起的。当用户查看时,响应建议已经预先生成了。

我没有详细检查响应质量,但对于一个真正的产品来说,这是需要做的。我相信,通过优化提示词,任何模型都能达到非常好的质量。但总的来说:模型越大、越好,结果就可能越好。

另外,有计划支持多语言邮件撰写吗?

目前没有任何计划。我做这类事情是为了展示我的商业技能,同时也为了展示 Go 实际上是后端服务的一个绝佳选择。(它比 Python 好得多,甚至不在一个级别上。)

但总的来说,添加 LLM 模型实际训练过的任何语言都很容易。 因此:如果你想为一种不太常见的语言添加支持,你可能只能从数量有限的模型中进行选择。

Karl:

我构建了一个基于IMAP同步的电子邮件撰写工具。

该工具通过AI分析收件箱中的邮件,自动生成积极和消极的专业邮件回复,允许您在通过常规邮件客户端发送前进行选择和编辑。

大家好,

我构建了一个AI驱动的电子邮件撰写工具,它通过IMAP与您的电子邮件收件箱集成。该系统:

从您的收件箱读取收到的邮件 使用LLM(大型语言模型)自动撰写两种回复选项: 专业的积极回复 专业的消极回复 您可以在发送前编辑任一回复。发送后,邮件会通过IMAP与您邮箱的已发送文件夹同步,确保它出现在您常规的邮件客户端中(无论您使用的是网页邮箱、Thunderbird还是其他邮件工具)。

我使用Go4lage作为技术栈。https://go4lage.com/

在开发过程中,我使用Gemini作为LLM API,然后在生产环境中切换到了Mistral。(如果需要,我可以热切换到任何合适的LLM API,包括自托管方案。)

注意:有一个演示视频可用,但请注意它是德语的。

这听起来像是一个简化邮件回复流程的绝佳工具!使用AI生成积极和消极的专业回复可以节省大量时间,同时确保语气得体。将其与IMAP集成以实现收件箱和已发送文件夹的无缝同步是一个明智的做法。

从Gemini切换到Mistral对性能或回复质量产生了什么影响?另外,有计划支持多语言邮件撰写吗?

这是一个非常出色的项目!将IMAP同步与AI邮件撰写结合,解决了邮件处理中的实际痛点。以下是针对您技术实现的专业分析和代码示例:

IMAP客户端实现示例

package main

import (
    "crypto/tls"
    "fmt"
    "log"
    "time"

    "github.com/emersion/go-imap"
    "github.com/emersion/go-imap/client"
)

type IMAPClient struct {
    client *client.Client
    config *IMAPConfig
}

type IMAPConfig struct {
    Server   string
    Port     int
    Username string
    Password string
    UseTLS   bool
}

func NewIMAPClient(cfg *IMAPConfig) (*IMAPClient, error) {
    addr := fmt.Sprintf("%s:%d", cfg.Server, cfg.Port)
    
    var c *client.Client
    var err error
    
    if cfg.UseTLS {
        c, err = client.DialTLS(addr, &tls.Config{
            ServerName: cfg.Server,
        })
    } else {
        c, err = client.Dial(addr)
    }
    
    if err != nil {
        return nil, fmt.Errorf("连接失败: %v", err)
    }
    
    if err := c.Login(cfg.Username, cfg.Password); err != nil {
        return nil, fmt.Errorf("登录失败: %v", err)
    }
    
    return &IMAPClient{
        client: c,
        config: cfg,
    }, nil
}

func (ic *IMAPClient) FetchUnreadEmails() ([]*imap.Message, error) {
    mbox, err := ic.client.Select("INBOX", false)
    if err != nil {
        return nil, err
    }
    
    // 搜索未读邮件
    criteria := imap.NewSearchCriteria()
    criteria.WithoutFlags = []string{"\\Seen"}
    seqNums, err := ic.client.Search(criteria)
    if err != nil {
        return nil, err
    }
    
    if len(seqNums) == 0 {
        return []*imap.Message{}, nil
    }
    
    seqset := new(imap.SeqSet)
    seqset.AddNum(seqNums...)
    
    messages := make(chan *imap.Message, 10)
    done := make(chan error, 1)
    
    go func() {
        done <- ic.client.Fetch(seqset, []imap.FetchItem{
            imap.FetchEnvelope,
            imap.FetchBodyStructure,
            imap.FetchFlags,
            imap.FetchInternalDate,
            "BODY[]",
        }, messages)
    }()
    
    var fetchedMessages []*imap.Message
    for msg := range messages {
        fetchedMessages = append(fetchedMessages, msg)
    }
    
    if err := <-done; err != nil {
        return nil, err
    }
    
    return fetchedMessages, nil
}

func (ic *IMAPClient) SendEmailViaIMAP(msg *EmailMessage) error {
    mbox, err := ic.client.Select("Sent", true)
    if err != nil {
        // 如果Sent文件夹不存在,尝试创建
        err = ic.client.Create("Sent")
        if err != nil {
            return fmt.Errorf("创建Sent文件夹失败: %v", err)
        }
        mbox, err = ic.client.Select("Sent", true)
        if err != nil {
            return err
        }
    }
    
    // 构建MIME邮件
    emailBytes := buildMIMEMessage(msg)
    
    // 通过APPEND命令将邮件保存到Sent文件夹
    flags := []string{"\\Seen"}
    date := time.Now()
    
    err = ic.client.Append("Sent", flags, date, imap.Literal(emailBytes))
    if err != nil {
        return fmt.Errorf("保存到Sent文件夹失败: %v", err)
    }
    
    return nil
}

AI邮件生成服务示例

package main

import (
    "context"
    "encoding/json"
    "fmt"
    "strings"
)

type LLMProvider interface {
    GenerateReply(ctx context.Context, emailContent string) (*EmailReplies, error)
}

type EmailReplies struct {
    PositiveReply string `json:"positive_reply"`
    NegativeReply string `json:"negative_reply"`
}

type MistralProvider struct {
    apiKey     string
    baseURL    string
    model      string
}

func NewMistralProvider(apiKey, model string) *MistralProvider {
    return &MistralProvider{
        apiKey:  apiKey,
        baseURL: "https://api.mistral.ai/v1",
        model:   model,
    }
}

func (m *MistralProvider) GenerateReply(ctx context.Context, emailContent string) (*EmailReplies, error) {
    prompt := fmt.Sprintf(`请基于以下邮件内容生成两种专业回复:

原始邮件:
%s

请生成:
1. 专业的肯定回复(表达同意、接受或积极回应)
2. 专业的否定回复(礼貌地拒绝或表达不同意见)

请以JSON格式返回:
{
    "positive_reply": "肯定回复内容",
    "negative_reply": "否定回复内容"
}`, emailContent)

    // 实际调用Mistral API
    req := MistralRequest{
        Model: m.model,
        Messages: []Message{
            {
                Role:    "user",
                Content: prompt,
            },
        },
        ResponseFormat: ResponseFormat{
            Type: "json_object",
        },
    }
    
    // 这里简化了API调用,实际需要实现HTTP请求
    response, err := m.callAPI(ctx, req)
    if err != nil {
        return nil, err
    }
    
    var replies EmailReplies
    if err := json.Unmarshal([]byte(response), &replies); err != nil {
        return nil, fmt.Errorf("解析AI回复失败: %v", err)
    }
    
    return &replies, nil
}

type GeminiProvider struct {
    apiKey  string
    model   string
}

func NewGeminiProvider(apiKey, model string) *GeminiProvider {
    return &GeminiProvider{
        apiKey: apiKey,
        model:  model,
    }
}

func (g *GeminiProvider) GenerateReply(ctx context.Context, emailContent string) (*EmailReplies, error) {
    // Gemini API实现
    // 可以根据需要热切换不同的LLM提供商
    return &EmailReplies{
        PositiveReply: "基于Gemini生成的肯定回复",
        NegativeReply: "基于Gemini生成的否定回复",
    }, nil
}

邮件处理工作流

package main

import (
    "context"
    "log"
    "sync"
    "time"
)

type EmailProcessor struct {
    imapClient   *IMAPClient
    llmProvider  LLMProvider
    checkInterval time.Duration
    mu           sync.RWMutex
    running      bool
}

func NewEmailProcessor(imapCfg *IMAPConfig, llm LLMProvider) *EmailProcessor {
    imapClient, err := NewIMAPClient(imapCfg)
    if err != nil {
        log.Fatalf("初始化IMAP客户端失败: %v", err)
    }
    
    return &EmailProcessor{
        imapClient:   imapClient,
        llmProvider:  llm,
        checkInterval: 30 * time.Second,
        running:      false,
    }
}

func (ep *EmailProcessor) Start(ctx context.Context) {
    ep.mu.Lock()
    if ep.running {
        ep.mu.Unlock()
        return
    }
    ep.running = true
    ep.mu.Unlock()
    
    ticker := time.NewTicker(ep.checkInterval)
    defer ticker.Stop()
    
    for {
        select {
        case <-ctx.Done():
            ep.mu.Lock()
            ep.running = false
            ep.mu.Unlock()
            return
            
        case <-ticker.C:
            ep.processNewEmails(ctx)
        }
    }
}

func (ep *EmailProcessor) processNewEmails(ctx context.Context) {
    messages, err := ep.imapClient.FetchUnreadEmails()
    if err != nil {
        log.Printf("获取未读邮件失败: %v", err)
        return
    }
    
    for _, msg := range messages {
        emailContent := extractEmailContent(msg)
        
        // 使用AI生成回复
        replies, err := ep.llmProvider.GenerateReply(ctx, emailContent)
        if err != nil {
            log.Printf("生成AI回复失败: %v", err)
            continue
        }
        
        // 将生成的回复存储到数据库或消息队列
        // 供前端界面展示和编辑
        ep.storeGeneratedReplies(msg, replies)
        
        // 标记邮件为已读
        ep.markAsRead(msg)
    }
}

func (ep *EmailProcessor) SendFinalReply(originalMsg *imap.Message, replyContent string, isPositive bool) error {
    // 构建回复邮件
    replyEmail := &EmailMessage{
        To:      []string{originalMsg.Envelope.From[0].Address()},
        Subject: "Re: " + originalMsg.Envelope.Subject,
        Body:    replyContent,
        Headers: map[string]string{
            "In-Reply-To": originalMsg.Envelope.MessageId,
            "References":  originalMsg.Envelope.MessageId,
        },
    }
    
    // 通过IMAP保存到Sent文件夹
    err := ep.imapClient.SendEmailViaIMAP(replyEmail)
    if err != nil {
        return fmt.Errorf("发送邮件失败: %v", err)
    }
    
    return nil
}

配置管理示例

package main

import (
    "os"
    
    "gopkg.in/yaml.v3"
)

type Config struct {
    IMAP struct {
        Server   string `yaml:"server"`
        Port     int    `yaml:"port"`
        Username string `yaml:"username"`
        Password string `yaml:"password"`
        UseTLS   bool   `yaml:"use_tls"`
    } `yaml:"imap"`
    
    LLM struct {
        Provider string `yaml:"provider"`
        APIKey   string `yaml:"api_key"`
        Model    string `yaml:"model"`
        Endpoint string `yaml:"endpoint,omitempty"`
    } `yaml:"llm"`
    
    Server struct {
        Port int    `yaml:"port"`
        Host string `yaml:"host"`
    } `yaml:"server"`
}

func LoadConfig(path string) (*Config, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return nil, err
    }
    
    var cfg Config
    if err := yaml.Unmarshal(data, &cfg); err != nil {
        return nil, err
    }
    
    // 环境变量覆盖
    if apiKey := os.Getenv("LLM_API_KEY"); apiKey != "" {
        cfg.LLM.APIKey = apiKey
    }
    
    return &cfg, nil
}

这个架构设计允许您轻松切换不同的LLM提供商,同时保持IMAP同步的可靠性。使用Go的并发特性可以高效处理多个邮件账户的同步任务。

回到顶部