mdsend

package module
v0.1.9 Latest Latest
Warning

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

Go to latest
Published: Jul 17, 2026 License: MIT Imports: 31 Imported by: 0

README

Mdsend

Go Reference

Send markdown files as electronic mail. Maintain mailing lists as templated text files.

Features

  • Durable: mail queues are fault tolerant and atomic, brokered by <www.watermill.io> over SQLite3. Can handle any volume of mail without degredation.
  • Portable: runs on many kinds of systems. Will include an embeddable HTTP service and Posgres support in the future.
  • Flexible: select the mailer backend based on highest deliverability. Swap it out later without changing anything in your letters or templates.
    • Supports recipient list formats: CSV, JSON, YAML, TOML, and Cue.
    • Supports mailing services:
      • SMTP (SMTP_HOST, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD)
      • Mailgun (MG_API_KEY, MG_API_DOMAIN) or SMTP relay
      • Resend via SMTP relay or (RESEND_API_KEY)
      • Amazon SES (requires AWS configuration present)
        • The author decoupled himself from AWS years ago and requests help testing this implementation.
      • Loops
    • Supports additional mailing services by SMTP relay:
      • HubSpot? (smtp.hubapi.com)
      • SMTP2GO
      • Brevo
      • Mailchimp
      • Twilio
      • SparkPost
      • Postmark
      • SendGrid
      • ZeptoMail
      • SendPulse
      • MailTrap
      • MailJet
      • EmailLabs
      • PurelyMail

SMTP relay is often more robust than the provider API. For example, Resend API does not support multi-value headers, but its SMTP relay does.

Examples

Compose a letter as a Markdown file. It must include the subject, the sender, and at least one recipient in the frontmatter:

---
subject: "Test Email"
from: "Sender <test@gmail.com>"
to: "Recipient <test@gmail.com>"
---

# Title of the Example Letter

Write text in **Markdown** notation.

Provide server credentials and point mdsend at the saved file.

export SMTP_HOST=...
export SMTP_PORT=...
export SMTP_USERNAME=...
export SMTP_PASSWORD=...
mdsend send letter.md

More usage examples are here:

Installation

  • MacOS:
    brew tap dkotik/tap
    brew install curl mdsend
    
  • Build from source:
    go install github.com/dkotik/mdsend@latest
    

Development

Mdsend is under active development. Version 1.0.0 is expected by the end of 2026.

Click here to see a list of planned features.
  • add --from flag to send command that uses middleware to override sender address, might be required by certain delivery methods
  • body template should be HTML template
  • Per RFC 5321 rules, email body text lines must not exceed 1,000 characters.
  • service package with HTTP unsubscribe endpoint package
  • list out templating functions in the example, including the default ones
  • mdsend should be equivalent to mdsend send ?
  • Queue clean up scanner - should be first added to sqlite Watermill driver.
  • Sending message with scheduling delay (instead of only queuing) should prompt a confirmation.
  • Ensure carbon copy list is in header?
  • Write a better Goldmark 2.0 renderer that recognizes single-line youtube and image paragraphs, centers them.
  • Beautify the default template. Add dark.html one.
  • Validate function should detect language and complain that language field is not set, if the content is not English.
  • Event invitations markup: https://developers.google.com/gmail/markup/reference/event-reservation#basic_event_reminder_without_a_ticket
  • https://github.com/charmbracelet/glamour
  • Publish Debian package to JFrog Artifactory or similar.
  • CLI duration parser it its own from imported package in schedule.go.
    • Add y and mo duration units and remove the sub-second ones.
  • Run some tests with https://www.suped.com/tools/email-tester
  • add to https://github.com/rothgar/awesome-tuis and bubbletea list of apps

E-mail Tools

Similar Projects

Documentation

Index

Constants

View Source
const (
	FieldNameID                        = "id"
	FieldNameSeed                      = "seed"
	FieldNameExtends                   = "extends"
	FieldNameDatabase                  = "queue"
	FieldNameSubject                   = "subject"
	FieldNameReplyTo                   = "reply_to"
	FieldNameAttachments               = "attachments"
	FieldNameAttachmentName            = "name"
	FieldNameAttachmentLocation        = "location"
	FieldNameTemplates                 = "templates"
	FieldNameHeaders                   = "headers"
	FieldNameLanguage                  = "language"
	FieldNameMediaContraints           = "media_constraints"
	FieldNameMediaConstraintsQuality   = "quality"
	FieldNameMediaConstrainsResolution = "resolution"
	FieldNameMediaConstraintsWidth     = "width"
	FieldNameMediaConstrainsHeight     = "height"
	FieldNameSchedule                  = "schedule"
	FieldNameScheduleAfter             = "after"
	FieldNameScheduleDelay             = "delay"
	FieldNameScheduleStep              = "step"
	FieldNameScheduleExpire            = "expire"
	FieldNameScheduleFluctuate         = "fluctuate"
)
View Source
const Version = "dev"

Variables

This section is empty.

Functions

func EachAttachmentSourceRelativeWithPathPrefix added in v0.0.5

func EachAttachmentSourceRelativeWithPathPrefix(prefix string, each iter.Seq2[AttachmentSource, error]) iter.Seq2[AttachmentSource, error]

Types

type Attachment

type Attachment struct {
	LetterID string
	Name     string
	Source   string
	// Hash                     uint64 // for XXHash2
	Hash string

	// ContentID is the ID of the inline attachment to use in the message.
	// It must conform to RFC 2392 format, including the angle brackets:
	// <hash@domain.com>. Leave empty for standalone attachments that
	// are not referenced by other parts of the message.
	ContentID   string
	ContentType string
	Content     []byte
}

func NewAttachment

func NewAttachment(b []byte, constraints media.Constraints) (a Attachment, err error)

func NewAttachmentFromFile added in v0.0.5

func NewAttachmentFromFile(fs fs.FS, p string, constraints media.Constraints) (a Attachment, err error)

func (Attachment) AssertEqualityTo added in v0.0.5

func (a Attachment) AssertEqualityTo(b Attachment) error

func (Attachment) IsEqualTo added in v0.0.5

func (a Attachment) IsEqualTo(b Attachment) bool

func (Attachment) LogValue added in v0.0.5

func (a Attachment) LogValue() slog.Value

func (Attachment) Validate added in v0.0.5

func (a Attachment) Validate() (err error)

func (Attachment) WithUpdatedHash added in v0.0.5

func (a Attachment) WithUpdatedHash() Attachment

type AttachmentError added in v0.0.5

type AttachmentError uint8
const (
	ErrInvalidAttachment AttachmentError = iota
	ErrDuplicateAttachment
)

func (AttachmentError) Error added in v0.0.5

func (err AttachmentError) Error() string

type AttachmentReference added in v0.0.5

type AttachmentReference struct {
	ID          string
	Name        string
	ContentID   string
	ContentType string
}

type AttachmentSource added in v0.0.5

type AttachmentSource struct {
	Name     string
	Location string
}

type FieldComparisonMismatchError added in v0.0.5

type FieldComparisonMismatchError struct {
	FieldName     string
	ExpectedValue any
	ActualValue   any
}

func (FieldComparisonMismatchError) Error added in v0.0.5

type Letter added in v0.0.5

type Letter struct {
	ID string
	// Frontmatter orderedmap.OrderedMap[string, any]
	Frontmatter map[string]any
	Templates   []Attachment
	Content     string
	CreatedAt   time.Time
	SentAt      time.Time
}

func NewLetter added in v0.0.5

func NewLetter(b []byte) (letter Letter, err error)

func NewLetterFromFile added in v0.0.5

func NewLetterFromFile(
	ctx context.Context,
	fs fs.FS,
	p string,
) (letter Letter, err error)

func (Letter) AssertEqualityTo added in v0.0.5

func (l Letter) AssertEqualityTo(b Letter) error

func (Letter) EachAttachmentSource added in v0.0.5

func (l Letter) EachAttachmentSource() iter.Seq2[AttachmentSource, error]

func (Letter) EachWarning added in v0.0.5

func (l Letter) EachWarning(fs fs.FS) iter.Seq2[*i18n.LocalizeConfig, error]

func (Letter) GetDatabase added in v0.0.5

func (l Letter) GetDatabase() string

func (Letter) GetExpiration added in v0.1.0

func (l Letter) GetExpiration() (t time.Duration, err error)

func (Letter) GetFrom added in v0.0.5

func (l Letter) GetFrom() (mail.Address, error)

GetFrom returns the address.FieldFrom address from the frontmatter. There must be only one valid address. Mutliple from addresses can disrupt delivery.

func (Letter) GetHeaders added in v0.0.5

func (l Letter) GetHeaders() (headers []header.Header, err error)

func (Letter) GetLanguage added in v0.0.5

func (l Letter) GetLanguage() (lang language.Tag, err error)

func (Letter) GetMediaConstraints added in v0.0.5

func (l Letter) GetMediaConstraints() (m media.Constraints, err error)

func (Letter) GetSchedule added in v0.0.5

func (l Letter) GetSchedule() (s Schedule, err error)

func (Letter) GetSeed added in v0.0.5

func (l Letter) GetSeed() (string, error)

func (Letter) GetSubject added in v0.0.5

func (l Letter) GetSubject() (string, error)

func (Letter) IsEqualTo added in v0.0.5

func (l Letter) IsEqualTo(b Letter) bool

func (Letter) IsValid added in v0.0.5

func (l Letter) IsValid(cxt context.Context, logger *slog.Logger) (ok bool)

func (Letter) LogValue added in v0.0.5

func (l Letter) LogValue() slog.Value

func (Letter) Validate added in v0.0.5

func (l Letter) Validate() (err error)

type LetterError added in v0.0.5

type LetterError uint
const (
	ErrNoSubject LetterError = iota + 1
	// ErrNoQueue
	ErrDuplicateLetter
	ErrNoFromAddress
	ErrNoContent
	ErrNotFound
	ErrFieldNotFound
)

func (LetterError) Error added in v0.0.5

func (l LetterError) Error() string

type Mailer

type Mailer interface {
	SendMail(context.Context, Message) (string, error)
}

type Message

type Message struct {
	ID            string
	LetterID      string
	SeedKey       string
	Headers       []header.Header
	From          mail.Address
	To            mail.Address
	Subject       string
	Text          string
	HTML          string
	ScheduleAfter time.Time
	ScheduledAt   time.Time
	SentAt        time.Time
}

Message is an intent to delivery a copy of a letter to a particular recipient.

func (Message) AssertEqualityTo added in v0.0.5

func (m Message) AssertEqualityTo(b Message) error

func (Message) IsEqualTo added in v0.0.5

func (m Message) IsEqualTo(b Message) bool

func (Message) LogValue added in v0.0.5

func (m Message) LogValue() slog.Value

func (Message) Validate added in v0.0.5

func (m Message) Validate() (err error)

type MessageError added in v0.0.5

type MessageError uint8
const (
	ErrInvalidMessage MessageError = iota
	ErrDuplicateMessage
	ErrMessageNotFound
)

func (MessageError) Error added in v0.0.5

func (err MessageError) Error() string

type Schedule added in v0.0.5

type Schedule struct {
	After     time.Time     `json:"after"`
	Step      time.Duration `json:"step"`
	Fluctuate time.Duration `json:"fluctuate"`
	Expire    time.Duration `json:"expire"`
}

func (Schedule) Next added in v0.0.5

func (s Schedule) Next() Schedule

Directories

Path Synopsis
cmd
mdsend command
mime
Package mime encodes electronic mail parts for delivery.
Package mime encodes electronic mail parts for delivery.
smtp
Package smtp sends MIME-formatted emails via SMTP.
Package smtp sends MIME-formatted emails via SMTP.

Jump to

Keyboard shortcuts

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