[GH-ISSUE #643] Feature Request: API emulation for common email providers (SendGrid/Postmark) #405

Closed
opened 2026-03-15 14:16:18 +03:00 by kerem · 4 comments
Owner

Originally created by @atkawa7 on GitHub (Feb 12, 2026).
Original GitHub issue: https://github.com/axllent/mailpit/issues/643

Summary

Add API compatibility / emulation mode for popular transactional email providers such as SendGrid and Postmark, allowing Maipit to act as a drop-in replacement.

Problem

Many applications are already tightly integrated with SendGrid/Postmark APIs and SDKs. Migrating to Maipit currently requires application code changes, which increases adoption friction.

Providing compatible endpoints would allow developers to switch providers with minimal effort.

Proposed Solution

Introduce an API emulation layer that mimics commonly used endpoints and request/response formats of major email providers.

Initial Scope

SendGrid compatibility

Support commonly used endpoints such as:

  • POST /v3/mail/send
  • Basic API key authentication (Authorization: Bearer)
  • Event webhook payload compatibility (delivery, bounce, open, click)

Postmark compatibility

Support commonly used endpoints such as:

  • POST /email
  • POST /email/withTemplate
  • Header compatibility (X-Postmark-Server-Token)
  • Webhook event payload compatibility
Originally created by @atkawa7 on GitHub (Feb 12, 2026). Original GitHub issue: https://github.com/axllent/mailpit/issues/643 ### Summary Add **API compatibility / emulation mode** for popular transactional email providers such as **SendGrid** and **Postmark**, allowing Maipit to act as a drop-in replacement. ### Problem Many applications are already tightly integrated with SendGrid/Postmark APIs and SDKs. Migrating to Maipit currently requires application code changes, which increases adoption friction. Providing compatible endpoints would allow developers to switch providers with minimal effort. ### Proposed Solution Introduce an **API emulation layer** that mimics commonly used endpoints and request/response formats of major email providers. ### Initial Scope #### SendGrid compatibility Support commonly used endpoints such as: * `POST /v3/mail/send` * Basic API key authentication (`Authorization: Bearer`) * Event webhook payload compatibility (delivery, bounce, open, click) #### Postmark compatibility Support commonly used endpoints such as: * `POST /email` * `POST /email/withTemplate` * Header compatibility (`X-Postmark-Server-Token`) * Webhook event payload compatibility
kerem closed this issue 2026-03-15 14:16:23 +03:00
Author
Owner

@axllent commented on GitHub (Feb 13, 2026):

Hi @atkawa7 - this topic has been discussed a few times in the past (for example in #167 & #278) and my reasons for rejecting it are explained there. Whilst I completely understand your request, the feasibility of building and maintaining all the various APIs is not realistic. The suggested approach would be to build your own bridge to Mailpit which mocks those APIs.

<!-- gh-comment-id:3894984877 --> @axllent commented on GitHub (Feb 13, 2026): Hi @atkawa7 - this topic has been discussed a few times in the past (for example in #167 & #278) and my reasons for rejecting it are explained there. Whilst I completely understand your request, the feasibility of building and maintaining all the various APIs is not realistic. The suggested approach would be to build your own bridge to Mailpit which mocks those APIs.
Author
Owner

@atkawa7 commented on GitHub (Feb 13, 2026):

@axllent Thanks for the clarification - that makes sense regarding the maintenance burden of supporting multiple APIs directly in Mailpit.

Instead of adding those APIs into Mailpit itself, would you be open to a different approach: a small plugin framework foundation that allows users to extend Mailpit externally? The idea would be to avoid copying or forking Mailpit and instead let the community build bridges (e.g. SendGrid/Postmark emulation) as plugins that can register additional HTTP routes and integrate with Mailpit at runtime.

This way the core project stays focused and lightweight, while extensions can live and evolve independently. Would this direction be something you’d be open to discussing?

<!-- gh-comment-id:3895031237 --> @atkawa7 commented on GitHub (Feb 13, 2026): @axllent Thanks for the clarification - that makes sense regarding the maintenance burden of supporting multiple APIs directly in Mailpit. Instead of adding those APIs into Mailpit itself, would you be open to a different approach: a small plugin framework foundation that allows users to extend Mailpit externally? The idea would be to avoid copying or forking Mailpit and instead let the community build bridges (e.g. SendGrid/Postmark emulation) as plugins that can register additional HTTP routes and integrate with Mailpit at runtime. This way the core project stays focused and lightweight, while extensions can live and evolve independently. Would this direction be something you’d be open to discussing?
Author
Owner

@axllent commented on GitHub (Feb 13, 2026):

@atkawa7 - I may be open to considering it, but I would first need to investigate plugin handling in Go (generally), something I do not have time for in the next couple of months. Furthermore, I expect that with such an approach it will still result in much more overheads and considerations when implementing code changes within Mailpit (compared to now).

Could you please explain to me why a standalone "bridge" is a bad idea? This bridge can be written in any language and simply delivers the message to Mailpit over SMPT. Sure, the downside is it is a standalone service, but other than that I only see advantages.

<!-- gh-comment-id:3895535247 --> @axllent commented on GitHub (Feb 13, 2026): @atkawa7 - I _may_ be open to considering it, but I would first need to investigate plugin handling in Go (generally), something I do not have time for in the next couple of months. Furthermore, I expect that with such an approach it will still result in much more overheads and considerations when implementing code changes within Mailpit (compared to now). Could you please explain to me why a standalone "bridge" is a bad idea? This bridge can be written in any language and simply delivers the message to Mailpit over SMPT. Sure, the downside is it is a standalone service, but other than that I only see advantages.
Author
Owner

@atkawa7 commented on GitHub (Feb 22, 2026):

@axllent

I’ve taken some time to explore this further, and the approach looks viable. For the initial implementation, I’m using github.com/hashicorp/go-plugin as the foundation.

Proposed Architecture

The idea is:

  • Plugin source code lives under plugins/
  • Compiled plugins are placed in /app/bin/plugins at runtime
  • Each plugin runs as an independent process (separate from Mailpit)

Using this model, plugins are fully isolated from Mailpit. They communicate over net/rpc, with go-plugin handling process lifecycle, handshake validation, and RPC wiring.

This keeps Mailpit lightweight while allowing plugins to be developed, built, and deployed independently.


How Mailpit Loads Plugins

At startup, Mailpit would:

  1. Discover plugin binaries in /app/bin/plugins
  2. Launch each plugin process via go-plugin
  3. Register their routes dynamically into the main router

Conceptually, something like:

r := mux.NewRouter()

var pluginRegistry []PluginInfo
pluginMap := map[string]LoadedPlugin{}
openAPIPaths := map[string]interface{}{}

for _, p := range plugins {
    pluginName := p.Impl.Name()
    pluginMap[pluginName] = p

    var registeredRoutes []string

    for _, route := range p.Impl.Routes() {
        routePath := fmt.Sprintf("/%s%s", pluginName, route.Path)

        if route.Path == "" || route.Path == "/" {
            routePath = fmt.Sprintf("/%s*", pluginName)
        }

        // Register route with main router
        r.HandleFunc(routePath, route.Handler)
        registeredRoutes = append(registeredRoutes, routePath)
    }

    pluginRegistry = append(pluginRegistry, PluginInfo{
        Name:   pluginName,
        Routes: registeredRoutes,
    })
}

This approach:

  • Namespaces routes per plugin (/pluginName/...)
  • Allows dynamic OpenAPI aggregation
  • Keeps plugin routing logic self-contained

Example Plugin

A minimal plugin implementation could look like this:

package main

import (
	"encoding/gob"
	"os"

	"github.com/hashicorp/go-hclog"
	"github.com/hashicorp/go-plugin"
)

func main() {
	gob.Register(map[string]string{})

	handshake := plugin.HandshakeConfig{
		ProtocolVersion:  1,
		MagicCookieKey:   "MAILPIT_PLUGIN",
		MagicCookieValue: "yes",
	}

	logger := hclog.New(&hclog.LoggerOptions{
		Name:   "ses-plugin",
		Level:  hclog.LevelFromString("DEBUG"),
		Output: os.Stderr,
	})

	plugin.Serve(&plugin.ServeConfig{
		HandshakeConfig: handshake,
		Plugins: map[string]plugin.Plugin{
			"mailpit": &shared.PluginRPCPlugin{
				Impl: &SesPlugin{},
			},
		},
		Logger: logger,
	})
}
<!-- gh-comment-id:3940203354 --> @atkawa7 commented on GitHub (Feb 22, 2026): @axllent I’ve taken some time to explore this further, and the approach looks viable. For the initial implementation, I’m using `github.com/hashicorp/go-plugin` as the foundation. ### Proposed Architecture The idea is: * Plugin source code lives under `plugins/` * Compiled plugins are placed in `/app/bin/plugins` at runtime * Each plugin runs as an independent process (separate from Mailpit) Using this model, plugins are fully isolated from Mailpit. They communicate over `net/rpc`, with `go-plugin` handling process lifecycle, handshake validation, and RPC wiring. This keeps Mailpit lightweight while allowing plugins to be developed, built, and deployed independently. --- ### How Mailpit Loads Plugins At startup, Mailpit would: 1. Discover plugin binaries in `/app/bin/plugins` 2. Launch each plugin process via `go-plugin` 3. Register their routes dynamically into the main router Conceptually, something like: ```go r := mux.NewRouter() var pluginRegistry []PluginInfo pluginMap := map[string]LoadedPlugin{} openAPIPaths := map[string]interface{}{} for _, p := range plugins { pluginName := p.Impl.Name() pluginMap[pluginName] = p var registeredRoutes []string for _, route := range p.Impl.Routes() { routePath := fmt.Sprintf("/%s%s", pluginName, route.Path) if route.Path == "" || route.Path == "/" { routePath = fmt.Sprintf("/%s*", pluginName) } // Register route with main router r.HandleFunc(routePath, route.Handler) registeredRoutes = append(registeredRoutes, routePath) } pluginRegistry = append(pluginRegistry, PluginInfo{ Name: pluginName, Routes: registeredRoutes, }) } ``` This approach: * Namespaces routes per plugin (`/pluginName/...`) * Allows dynamic OpenAPI aggregation * Keeps plugin routing logic self-contained --- ### Example Plugin A minimal plugin implementation could look like this: ```go package main import ( "encoding/gob" "os" "github.com/hashicorp/go-hclog" "github.com/hashicorp/go-plugin" ) func main() { gob.Register(map[string]string{}) handshake := plugin.HandshakeConfig{ ProtocolVersion: 1, MagicCookieKey: "MAILPIT_PLUGIN", MagicCookieValue: "yes", } logger := hclog.New(&hclog.LoggerOptions{ Name: "ses-plugin", Level: hclog.LevelFromString("DEBUG"), Output: os.Stderr, }) plugin.Serve(&plugin.ServeConfig{ HandshakeConfig: handshake, Plugins: map[string]plugin.Plugin{ "mailpit": &shared.PluginRPCPlugin{ Impl: &SesPlugin{}, }, }, Logger: logger, }) } ```
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/mailpit#405
No description provided.