Documentation
¶
Overview ¶
Package mail fetches one-time verification codes from an IMAP mailbox.
It is the kit home for the "wait for the OTP email, pull the code out of it" chore that almost every account-registration flow needs. The one type you use is CodeFetcher: open it once per mailbox at module start, then call CodeFetcher.FetchCode from your workers — requests are multiplexed over a single, kept-alive, auto-reconnecting connection, so hundreds of parallel accounts don't each open a socket.
Read the CodeFetcher doc for the *mailTime idiom* — capturing a timestamp before the action that triggers the send — which is what keeps you from matching a stale code on a reused or catch-all mailbox.
The client speaks IMAP over implicit TLS directly with no external dependency, and needs only the standard INTERNALDATE capability.
Index ¶
- Constants
- func ValidateModernFeatures(features *IMAPFeatures) error
- type CodeFetcher
- type EmailHeaders
- type EnvelopeData
- type FetchRequest
- type FetchResult
- type IMAPClient
- func (c *IMAPClient) Connect() error
- func (c *IMAPClient) DeleteByUID(uid string) error
- func (c *IMAPClient) Disconnect() error
- func (c *IMAPClient) EnableDebug()
- func (c *IMAPClient) FetchHeaders(emailUIDs []string) ([]EmailHeaders, error)
- func (c *IMAPClient) FetchMail(uid string) (*Mail, error)
- func (c *IMAPClient) IsDebug() bool
- func (c *IMAPClient) ListMailboxes() error
- func (c *IMAPClient) Noop() error
- func (c *IMAPClient) SearchEmails(criteria SearchCriteria, limit int) ([]string, error)
- func (c *IMAPClient) SelectInbox() error
- type IMAPFeatures
- type Mail
- type SearchCriteria
Examples ¶
Constants ¶
const ( FETCH_INTERVAL = 5 * time.Second // Check for new fetch requests every 5 seconds NOOP_INTERVAL = 15 * time.Second // Send NOOP command every 15 seconds to keep connection alive )
Constants for the main loop
Variables ¶
This section is empty.
Functions ¶
func ValidateModernFeatures ¶
func ValidateModernFeatures(features *IMAPFeatures) error
ValidateModernFeatures checks if the server supports required modern features
Types ¶
type CodeFetcher ¶
type CodeFetcher struct {
IMAPClient *IMAPClient
// contains filtered or unexported fields
}
CodeFetcher is a long-lived, concurrency-safe IMAP poller purpose-built for pulling one-time verification codes out of a shared mailbox. Create one per mailbox at module start (it opens a persistent connection, keeps it alive with NOOP, and transparently reconnects), then call CodeFetcher.FetchCode from every worker — requests are multiplexed over the single connection, so hundreds of parallel accounts don't each open a socket.
The mailTime idiom (read this) ¶
IMAP's SINCE search only has day granularity, so it cannot express "mail that arrived in the last minute". FetchCode instead filters on the server's INTERNALDATE (millisecond precision) client-side. To match the *fresh* code and never a stale one from a previous attempt on the same (often catch-all) mailbox, capture a timestamp immediately BEFORE the browser action that triggers the send, then pass it as sinceUnixMilli:
mailTime := time.Now().UnixMilli()
browser.Click(ctx, browserscale.CSS("#send-code")) // triggers the email
code, err := f.FetchCode(ctx, from, toEmail, "", codeRegex, mailTime-60_000, 60_000, true)
Subtract a skew buffer (~60s) because the mail server's clock and the local clock rarely agree to the millisecond; without it a valid mail whose INTERNALDATE is a few seconds "before" mailTime is wrongly skipped.
func NewCodeFetcher ¶
func (*CodeFetcher) EnableDebug ¶
func (f *CodeFetcher) EnableDebug()
EnableDebug turns on verbose connection logging (reconnects, NOOP failures, non-matching mail bodies). Off by default so the library stays silent.
func (*CodeFetcher) FetchCode ¶
func (f *CodeFetcher) FetchCode(ctx context.Context, fromEmail string, toEmail string, subjectKeyword string, codeRegex string, sinceUnixMilli int64, maxSearchTimeMs int64, deleteAfterFetch bool) (string, error)
FetchCode polls the mailbox until a matching message arrives, extracts a code from its body and returns it — or errors on timeout. It blocks up to maxSearchTimeMs (default 5 min when 0) or until ctx is cancelled.
Parameters:
- fromEmail: sender to match (IMAP FROM). "" matches any sender.
- toEmail: recipient to match (IMAP TO) — the account's address on a catch-all mailbox. "" matches any recipient.
- subjectKeyword: optional IMAP SUBJECT filter. "" to skip.
- codeRegex: Go regexp with ONE capture group; the first submatch is returned (e.g. `(\d{6})` for a 6-digit OTP).
- sinceUnixMilli: only consider mail whose INTERNALDATE >= this. Pass the mailTime captured before the trigger, minus a ~60s skew (see the type doc); 0 disables the filter (risking a stale code on a reused mailbox).
- maxSearchTimeMs: overall budget for this fetch.
- deleteAfterFetch: expunge the matched mail once the code is read (keeps a shared mailbox clean so the next attempt can't re-match it).
Example ¶
ExampleCodeFetcher_FetchCode shows the mailTime idiom: capture the timestamp BEFORE the action that triggers the send, then pass it (minus a skew buffer) as sinceUnixMilli so only the fresh code matches — never a stale one left in a reused / catch-all mailbox.
package main
import (
"context"
"time"
"github.com/browserscale/browserscale-kit/mail"
)
// triggerSend stands in for the browser action that makes the site send its
// verification email, e.g. browser.Click(ctx, browserscale.CSS("#send-code")).
func triggerSend() error { return nil }
// ExampleCodeFetcher_FetchCode shows the mailTime idiom: capture the timestamp
// BEFORE the action that triggers the send, then pass it (minus a skew buffer)
// as sinceUnixMilli so only the fresh code matches — never a stale one left in
// a reused / catch-all mailbox.
func main() {
f, err := mail.NewCodeFetcher("imap.example.com", 993, "catchall@example.com", "app-password")
if err != nil {
panic(err)
}
defer f.Stop()
mailTime := time.Now().UnixMilli() // before the trigger
if err := triggerSend(); err != nil {
panic(err)
}
code, err := f.FetchCode(
context.Background(),
"noreply@site.com", // fromEmail — the sender to match
"user+123@example.com", // toEmail — this account's address on the catch-all
"", // subjectKeyword — none
`(\d{6})`, // codeRegex — one capture group (6-digit OTP)
mailTime-60_000, // sinceUnixMilli — mailTime minus ~60s clock skew
60_000, // maxSearchTimeMs — 60s budget
true, // deleteAfterFetch — expunge once read
)
if err != nil {
panic(err)
}
_ = code
}
Output:
type EmailHeaders ¶
type EmailHeaders struct {
UID string
Subject string
From string
To string
Date string
InternalDate int64 // Unix milliseconds
}
EmailHeaders represents email header information including INTERNALDATE
type EnvelopeData ¶
EnvelopeData represents parsed envelope information
type FetchRequest ¶
type FetchRequest struct {
FromEmail string
ToEmail string
SubjectKeyword string
CodeRegex string
SinceUnixMilli int64
MaxSearchTime int64 // Timeout in milliseconds for how long to keep looking for the code
ResultChan chan FetchResult
DeleteAfterFetch bool
// contains filtered or unexported fields
}
type FetchResult ¶
type IMAPClient ¶
type IMAPClient struct {
// contains filtered or unexported fields
}
IMAPClient represents a simple IMAP client
func NewIMAPClient ¶
func NewIMAPClient(server string, port int, username string, password string) *IMAPClient
NewIMAPClient creates a new IMAP client for any server. TLS certificate verification is always skipped (for self-signed or corporate CA certificates).
func (*IMAPClient) Connect ¶
func (c *IMAPClient) Connect() error
Connect establishes a connection to IMAP server
func (*IMAPClient) DeleteByUID ¶
func (c *IMAPClient) DeleteByUID(uid string) error
DeleteByUID permanently removes a message by UID from the currently selected mailbox. Flow: mark \Deleted with UID STORE → try UID EXPUNGE → fallback to EXPUNGE.
func (*IMAPClient) Disconnect ¶
func (c *IMAPClient) Disconnect() error
Disconnect closes the connection (aggressive like Chilkat DisposeImap)
func (*IMAPClient) EnableDebug ¶
func (c *IMAPClient) EnableDebug()
EnableDebug enables debug output for raw send/receive data
func (*IMAPClient) FetchHeaders ¶
func (c *IMAPClient) FetchHeaders(emailUIDs []string) ([]EmailHeaders, error)
FetchHeaders fetches headers (including INTERNALDATE) for the given UIDs
func (*IMAPClient) FetchMail ¶
func (c *IMAPClient) FetchMail(uid string) (*Mail, error)
FetchMail fetches a complete email (headers and body) for a single UID
func (*IMAPClient) IsDebug ¶
func (c *IMAPClient) IsDebug() bool
IsDebug returns whether debug mode is enabled
func (*IMAPClient) ListMailboxes ¶
func (c *IMAPClient) ListMailboxes() error
ListMailboxes lists available mailboxes
func (*IMAPClient) Noop ¶
func (c *IMAPClient) Noop() error
Noop sends a NOOP command to keep the connection alive
func (*IMAPClient) SearchEmails ¶
func (c *IMAPClient) SearchEmails(criteria SearchCriteria, limit int) ([]string, error)
SearchEmails searches for emails using IMAP search capabilities and returns only UIDs
func (*IMAPClient) SelectInbox ¶
func (c *IMAPClient) SelectInbox() error
SelectInbox selects the INBOX
type IMAPFeatures ¶
type IMAPFeatures struct {
SupportsInternalDate bool
SupportsUIDPlus bool
SupportsCondStore bool
SupportsQResync bool
SupportsMove bool
SupportsBinary bool
SupportsPreview bool
SupportsSnippet bool
ServerInfo string
}
IMAPFeatures represents the capabilities of an IMAP server
func (*IMAPFeatures) PrintFeatures ¶
func (f *IMAPFeatures) PrintFeatures()
PrintFeatures prints the server capabilities in a readable format