CLI and Go package for Migadu
  • Go 99.8%
  • Pkl 0.2%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Tim Hårek Andreassen 0dc060c9de build: Move module path to src.timharek.no/tim/migadu
git.sr.ht is being decommissioned. The Forgejo instance serves the
go-import meta tag, so tag-based 'go get' keeps working under the new
path. Docs updated to match.
2026-09-09 21:50:31 +02:00
.builds ci: Lint every file and run the race detector 2026-08-17 15:57:54 +02:00
cmd/migadu build: Move module path to src.timharek.no/tim/migadu 2026-09-09 21:50:31 +02:00
internal build: Move module path to src.timharek.no/tim/migadu 2026-09-09 21:50:31 +02:00
.gitignore dev: Remove old ignore 2026-08-17 16:50:07 +02:00
.golangci.toml lint: Fix 2026-08-17 12:26:57 +02:00
alias.go refactor: Use json/v2 2026-08-20 21:46:08 +02:00
alias_test.go refactor: Use json/v2 2026-08-20 21:46:08 +02:00
context_test.go feat: Take a context in every API call 2026-08-17 15:57:18 +02:00
CONTRIBUTING.md build: Move module path to src.timharek.no/tim/migadu 2026-09-09 21:50:31 +02:00
domain.go refactor: Use json/v2 2026-08-20 21:46:08 +02:00
forwarding.go refactor: Use json/v2 2026-08-20 21:46:08 +02:00
forwarding_test.go refactor: Use json/v2 2026-08-20 21:46:08 +02:00
go.mod build: Move module path to src.timharek.no/tim/migadu 2026-09-09 21:50:31 +02:00
go.sum chore: Replace go-isatty with golang.org/x/term 2026-08-28 13:33:37 +02:00
go.tool.mod build: Move module path to src.timharek.no/tim/migadu 2026-09-09 21:50:31 +02:00
go.tool.sum refactor: Prompt with plain reads instead of huh 2026-08-28 10:01:10 +02:00
hk.pkl build: Replace make with mise 2026-08-17 11:25:25 +02:00
identity.go refactor: Use json/v2 2026-08-20 21:46:08 +02:00
identity_test.go feat: Take a context in every API call 2026-08-17 15:57:18 +02:00
LICENSE.md Initial commit 2024-10-27 21:08:36 +01:00
mailbox.go refactor: Use json/v2 2026-08-20 21:46:08 +02:00
mailbox_test.go refactor: Use json/v2 2026-08-20 21:46:08 +02:00
migadu.go build: Move module path to src.timharek.no/tim/migadu 2026-09-09 21:50:31 +02:00
migadu_test.go refactor: Use json/v2 2026-08-20 21:46:08 +02:00
mise.toml build: Upgrade go 2026-08-20 15:36:52 +02:00
ratelimit_test.go feat: Take a context in every API call 2026-08-17 15:57:18 +02:00
README.md build: Move module path to src.timharek.no/tim/migadu 2026-09-09 21:50:31 +02:00
rewrite.go refactor: Use json/v2 2026-08-20 21:46:08 +02:00
rewrite_test.go refactor: Use json/v2 2026-08-20 21:46:08 +02:00
settings.go feat: Add generic email settings 2026-08-17 13:31:37 +02:00
settings_test.go feat: Add generic email settings 2026-08-17 13:31:37 +02:00
timeout_test.go feat: Take a context in every API call 2026-08-17 15:57:18 +02:00
transport_test.go feat: Take a context in every API call 2026-08-17 15:57:18 +02:00
types.go refactor: Use json/v2 2026-08-20 21:46:08 +02:00
types_test.go refactor: Use json/v2 2026-08-20 21:46:08 +02:00

Migadu

CLI for Migadu and Go-package for interacting with Migadu.

This project is not endorsed or affiliated by Migadu-Mail GmbH.

Install CLI

Using go:

go install src.timharek.no/tim/migadu@latest

Using Homebrew

brew tap timharek/tap
brew install migadu

Using pre-compiled binary

Select the version you want to install available releases and download it and add it to your bin or something similar.

Example:

wget https://src.timharek.no/tim/migadu/releases/download/<release>/migadu-<version>-linux-amd64.tar.gz
tar xf migadu-<version>-linux-amd64.tar.gz
cd migadu-<version>
cp migadu /usr/local/bin

Usage

# Configure your user
migadu user add

# Get help
migadu --help

# Listing all mailboxes for default domain
migadu mailbox list

# Listing all mailboxes for a domain
migadu mailbox list --domain example.org

# The IMAP/POP/SMTP settings an email client needs, for one mailbox
migadu settings admin

Creating and updating

Anything you create or update opens in $EDITOR by default, as a commented TOML buffer that explains every field, and asks before it sends anything:

# Opens a buffer, shows what it would create, asks, then creates it
migadu mailbox create

# The buffer for an update is the mailbox as it is now. Only the keys you
# change are sent
migadu mailbox update admin

Saving and exiting the editor brings up the confirmation:

- May send: yes
+ May send: no

Apply? [y/N/e=edit again/q=quit]

e reopens the buffer with your edits still in it, which is also what a buffer that does not parse offers. Emptying the buffer, or answering anything but y, sends nothing at all.

Passing flags is how you skip the editor and do it in one line, which is what a script wants:

migadu mailbox create --local admin --password-method invitation \
	--password-recovery tim@example.org

The rules, in short:

  • No flags, and a terminal to open an editor in: $EDITOR.
  • Any flag: no editor.
  • --edit: the editor, with the flags filled in for you first.
  • --no-edit, or no terminal, such as in a script or CI: flags only.

VISUAL is used before EDITOR, and vi if neither is set.

As a package

import (
	"context"

	"src.timharek.no/tim/migadu"
)

func whatever(ctx context.Context) {
	// User and token fall back to MIGADU_USER and MIGADU_USER_TOKEN.
	c, err := migadu.New(migadu.Credentials{})

	// Credentials are who you are; options are how the client talks.
	c, err = migadu.New(migadu.Credentials{},
		migadu.WithBaseURL(srv.URL+"/v1"),   // another API root, such as a test server
		migadu.WithTimeout(10*time.Second),  // per request, on top of the context's deadline
		migadu.WithHTTPClient(client),       // or bring your own transport entirely
		migadu.WithUserAgent("my-app/1.0"),  // "" sends none at all
		migadu.WithLogger(log.Printf),       // "GET url -> 200 OK" per request
	)

	// Every call takes a context and honours its cancellation. Without a
	// WithTimeout of your own, requests give up after migadu.DefaultTimeout.
	domains, err := c.Domains.List(ctx)

	// Everything living under one domain is reached through a scope.
	d := c.Domain("example.org")
	mailboxes, err := d.Mailbox.List(ctx)
	identities, err := d.Identity.List(ctx, "admin")
	forwardings, err := d.Forwarding.List(ctx, "admin")
	rewrites, err := d.Rewrite.List(ctx)

	// Migadu's generic client settings are constants, so no call is made.
	settings := migadu.ClientSettings("admin@example.org")

	// Updates only send the fields they are given.
	_, err = c.Domains.Update(ctx, "example.org", migadu.DomainUpdate{
		GreylistingEnabled: new(false),
	})

	// A failure the API explained is an *APIError, which is also how a rate
	// limit says how long to wait.
	var apiErr *migadu.APIError
	if errors.As(err, &apiErr) && apiErr.IsRateLimited() {
		time.Sleep(apiErr.RetryAfter)
	}

	// Use the data
}

Contributing

Anyone can contribute to Migadu. Please refer to the contribution guidelines.

Open pull requests on the forge, report bugs on the issue tracker.