mail

package
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 9, 2026 License: MIT Imports: 19 Imported by: 0

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

Examples

Constants

View Source
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 NewCodeFetcher(host string, port int, username string, password string) (*CodeFetcher, error)

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
}

func (*CodeFetcher) Stop

func (f *CodeFetcher) Stop()

Stop stops the CodeFetcher and disconnects

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

type EnvelopeData struct {
	Subject string
	From    string
	To      string
	Date    string
}

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 FetchResult struct {
	Code string
	Err  error
}

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

type Mail

type Mail struct {
	UID          string
	Subject      string
	From         string
	To           string
	Date         string
	InternalDate int64 // Unix milliseconds
	Body         string
}

Mail represents a complete email with headers and body

type SearchCriteria

type SearchCriteria struct {
	Subject string
	From    string
	To      string
	Body    string
	Since   int64 // Unix milliseconds
	Before  int64 // Unix milliseconds
}

SearchCriteria defines search parameters for IMAP search

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL