package email import ( "crypto/tls" "fmt" "net/url" "strconv" "strings" "time" "github.com/emersion/go-imap" "github.com/emersion/go-imap/client" ) // IMAPClient handles reading emails via IMAP type IMAPClient struct { server string port int username string password string useTLS bool } // EmailMessage represents an email in the inbox type EmailMessage struct { UID uint32 `json:"uid"` Subject string `json:"subject"` From string `json:"from"` To []string `json:"to"` Date time.Time `json:"date"` Body string `json:"body"` Preview string `json:"preview"` Read bool `json:"read"` Attachments int `json:"attachments"` } // NewIMAPClient creates a new IMAP client func NewIMAPClient(server string, port int, username, password string) *IMAPClient { return &IMAPClient{ server: server, port: port, username: username, password: password, useTLS: port == 993, } } // Connect establishes connection to IMAP server func (c *IMAPClient) Connect() (*client.Client, error) { addr := fmt.Sprintf("%s:%d", c.server, c.port) var cl *client.Client var err error if c.useTLS { cl, err = client.DialTLS(addr, &tls.Config{InsecureSkipVerify: true}) } else { cl, err = client.Dial(addr) } if err != nil { return nil, fmt.Errorf("dial imap: %w", err) } if err := cl.Login(c.username, c.password); err != nil { cl.Logout() return nil, fmt.Errorf("imap login: %w", err) } return cl, nil } // ListMessages fetches emails from inbox func (c *IMAPClient) ListMessages(limit int) ([]EmailMessage, error) { cl, err := c.Connect() if err != nil { return nil, err } defer cl.Logout() // Select INBOX mbox, err := cl.Select("INBOX", false) if err != nil { return nil, fmt.Errorf("select inbox: %w", err) } if mbox.Messages == 0 { return []EmailMessage{}, nil } // Fetch last N messages from := uint32(1) if mbox.Messages > uint32(limit) { from = mbox.Messages - uint32(limit) + 1 } seqset := new(imap.SeqSet) seqset.AddRange(from, mbox.Messages) messages := make(chan *imap.Message, 10) done := make(chan error, 1) go func() { done <- cl.Fetch(seqset, []imap.FetchItem{imap.FetchEnvelope, imap.FetchFlags, imap.FetchRFC822Text}, messages) }() var result []EmailMessage for msg := range messages { email := EmailMessage{ UID: msg.Uid, Subject: msg.Envelope.Subject, Date: msg.Envelope.Date, Read: !hasFlag(msg.Flags, imap.RecentFlag), } if len(msg.Envelope.From) > 0 { email.From = msg.Envelope.From[0].Address() } for _, to := range msg.Envelope.To { email.To = append(email.To, to.Address()) } // Extract preview from body for _, literal := range msg.Body { if buf := make([]byte, 0); literal != nil { buf = make([]byte, literal.Len()) n, _ := literal.Read(buf) if n > 0 { body := string(buf[:n]) email.Body = body email.Preview = truncate(stripHTML(body), 200) break } } } result = append(result, email) } if err := <-done; err != nil { return nil, fmt.Errorf("fetch messages: %w", err) } // Reverse to show newest first for i, j := 0, len(result)-1; i < j; i, j = i+1, j-1 { result[i], result[j] = result[j], result[i] } return result, nil } // GetMessage fetches a single email by UID func (c *IMAPClient) GetMessage(uid uint32) (*EmailMessage, error) { cl, err := c.Connect() if err != nil { return nil, err } defer cl.Logout() // Select INBOX if _, err := cl.Select("INBOX", false); err != nil { return nil, fmt.Errorf("select inbox: %w", err) } seqset := new(imap.SeqSet) seqset.AddNum(uid) messages := make(chan *imap.Message, 1) done := make(chan error, 1) go func() { done <- cl.Fetch(seqset, []imap.FetchItem{imap.FetchEnvelope, imap.FetchFlags, imap.FetchRFC822Text}, messages) }() var email *EmailMessage for msg := range messages { email = &EmailMessage{ UID: msg.Uid, Subject: msg.Envelope.Subject, Date: msg.Envelope.Date, Read: !hasFlag(msg.Flags, imap.RecentFlag), } if len(msg.Envelope.From) > 0 { email.From = msg.Envelope.From[0].Address() } for _, to := range msg.Envelope.To { email.To = append(email.To, to.Address()) } for _, literal := range msg.Body { if literal != nil { buf := make([]byte, literal.Len()) n, _ := literal.Read(buf) if n > 0 { body := string(buf[:n]) email.Body = body email.Preview = truncate(stripHTML(body), 200) break } } } } if err := <-done; err != nil { return nil, fmt.Errorf("fetch message: %w", err) } return email, nil } // MarkAsRead marks an email as read func (c *IMAPClient) MarkAsRead(uid uint32) error { cl, err := c.Connect() if err != nil { return err } defer cl.Logout() if _, err := cl.Select("INBOX", false); err != nil { return fmt.Errorf("select inbox: %w", err) } seqset := new(imap.SeqSet) seqset.AddNum(uid) item := imap.FormatFlagsOp(imap.AddFlags, true) flags := []interface{}{imap.SeenFlag} if err := cl.Store(seqset, item, flags, nil); err != nil { return fmt.Errorf("mark as read: %w", err) } return nil } // GetUnreadCount returns number of unread messages func (c *IMAPClient) GetUnreadCount() (int, error) { cl, err := c.Connect() if err != nil { return 0, err } defer cl.Logout() mbox, err := cl.Select("INBOX", false) if err != nil { return 0, fmt.Errorf("select inbox: %w", err) } return int(mbox.Unseen), nil } // Helper functions func hasFlag(flags []string, flag string) bool { for _, f := range flags { if f == flag { return true } } return false } func stripHTML(html string) string { // Simple HTML stripping result := html result = strings.ReplaceAll(result, "
", "\n") result = strings.ReplaceAll(result, "
", "\n") result = strings.ReplaceAll(result, "

", "\n") result = strings.ReplaceAll(result, "

", "") // Remove tags for { start := strings.Index(result, "<") if start == -1 { break } end := strings.Index(result[start:], ">") if end == -1 { break } result = result[:start] + result[start+end+1:] } // Decode HTML entities result = strings.ReplaceAll(result, " ", " ") result = strings.ReplaceAll(result, "<", "<") result = strings.ReplaceAll(result, ">", ">") result = strings.ReplaceAll(result, "&", "&") result = strings.ReplaceAll(result, """, "\"") return strings.TrimSpace(result) } func truncate(s string, maxLen int) string { if len(s) <= maxLen { return s } return s[:maxLen] + "..." } // ParseIMAPURL parses an IMAP URL like imaps://user:pass@server:993 func ParseIMAPURL(imapURL string) (*IMAPClient, error) { u, err := url.Parse(imapURL) if err != nil { return nil, err } password, _ := u.User.Password() port := 993 if u.Port() != "" { p, err := strconv.Atoi(u.Port()) if err != nil { return nil, err } port = p } return NewIMAPClient(u.Hostname(), port, u.User.Username(), password), nil }