-
Notifications
You must be signed in to change notification settings - Fork 274
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Modular design #15
Comments
Yeah, +1 from me. I wonder if we need some kind of |
Also we need to abstract away authentication |
Continuing idea of separation of backends from endpoints... Module-based maddy designModule conceptEach interface required by maddy for operation is provided by some object called "module". Each module gets its own unique name ( Endpoint listeners are modules too, they just don't implement any interface and just start listening on address from instance name after initialization. Here is the most minimal interface for any module: type Module interface {
// Unique module name. Used in configuration and in logs.
Name() string
// Returns module version. May be printed to log and probably exposed to clients using extensions like IMAP ID.
Version() string
}
type NewModule func(instName string, cfg caddyfile.???) error And here is generic syntax for configuration:
Each block of this form creates new instance of module. Failure in module initialization (error returned by NewModule) is a fatal error and maddy will terminate after it. 'storage' interfaceWhen you are storing information about email in modern world you definitely store it together with IMAP meta-data and in IMAP-friendly format. That's it. IMAP defines email storage structure. There is very small room for freedom. For this very reason we make "IMAP backend" and "storage" terms mean the same idea: Place where we can place emails and read it later. In this case, Basically, go-imap/backend.Backend interface with removed authentication. There are just IMAP extensions that modify/extend storage behavior or require knowledge of its state are handled here. These extensions are enabled on endpoint-level only if they are supported by 'storage' implementation. 'auth' interfacetype AuthProvider interface {
CheckPlain(username, password string) bool
} 'filter' interfaceUsed in SMTP pipeline to mutate or drop messages during processing. type Filter interface {
// Allowed to change body. If returns false - message is dropped.
// opts are optional "context values" set in configuration, can be used to tweak
// filter behavior on per-message basis.
// if it returns non-nil error - message is dropped
Apply(ctx *DeliveryContext, body *bytes.Buffer) error
} Here DeliveryContext is a structure that contains basic information about SMTP client, SMTP envelope information (FROM, RCPT), opts set in configuration and arbitrary values that may be set by other filters. 'delivery' interfaceBasically the same as filter except it is not allowed to change anything.
'imap' moduleConfiguration options: SMTP pipelineSMTP doesn't creates any restrictions on how we can process email. SMTP is basically "I give you that message and I want it to be seen by Alice and Bob, do whatever you need to get this done". So we define "SMTP pipeline" concept here: Sequence of module instances that can transform messages how they want or probably save it somewhere or send it to a different server or all this at once. This allows users to construct infinitely complex chains to describe any logic they need. There are several variables (like 'filter' step applies instance_name filter to message, passing specified opts as first argument.
'delivery' step pushes message to instance_name SMTP backend and continues processing (this is necessary to correctly support multiple recipients both local and remote).
Pipeline steps wrapped in
Adding Value-name can be one of these:
Obviously, this stops processing:
Continue processing if client is logged in anonymously or using account from auth. provider. Otherwise - send "access denied" error and stop.
Require successful authentication using account from set auth. provider (
Config exampleHere is example of complete IMAP+SMTP server configuration:
This is nowhere complete proposal, just dumping some ideas for discussion. Any questions, additions or related ideas? |
I would love to see support for a flexible pipeline as not all SMTP servers deliver emails to an end client via IMAP. Here is the main use-case I have been working on:
This would also allow many creative types of email delivery (IPFS storage, encryption, posting articles to a blog) in addition to queued processing for automated systems (logging, ticket creation, NLP, etc...) If storage is flexible, and the pipeline chain-able, I can parse a streaming MIME message body in a memory safe way, encrypt the payloads, store it on S3 and further process it from another system. |
@emersion, I would like to know your opinion on proposed design. I think I'm done with basic ideas. Will start experimenting with implementation of ideas stated above in my maddy fork. |
Alright, here we are hit by limitations of Caddyfile format. What should we do? I would really like to switch to a different config format but I think it diverges too much from emersion's ideas about Caddy-like server. |
The overall goal of the configuration format is to keep it as simple as possible while still allowing for more complex (not too complex) scenario. An example would be your I'd also like to make things secure by default: no need to configure complex pipelines to get DKIM. A problem with that is loosing customizability. Thoughts? I'll try to think of a better approach. Your current approach looks pretty reasonable regardless. I think it'd be best to experiment with it and adjust it as needed. Here are a few more minor comments: We can probably simplify Deliver(ctx DeliveryContext, body bytes.Buffer) error I'd prefer to use streaming interfaces ( Sorry for taking so long to give feedback. While I'm pretty busy with IRL stuff right now (moving to a different country), I'd like to contribute too. Things will likely slow down in the next days/weeks. If you want, you can join the |
I'm fine with using a different parser btw. Caddy's is tedious to use imho. |
I guess we can have reasonably default pipeline configuration while still allowing user to redefine it if they are ok with increased complexity. Also we can get default set of backends (say, go-sqlmail with sqlite3 configured to store stuff at /var/maddy/messages.db).
Expanding to something similar to what I shown as full config example. This approach preserves full flexibility while making maddy almost zero-configuration.
Except that it probably should be DKIM instance with "dkim" name because instance names should be unique.
Probably we can pass io.Reader and io.Writer to filters. |
Support for additional SASL authentication methodsUsed authentication module should implement at least plaintext authentication. type OAuthAuth interface {
CheckOAuth(username, token string) bool
} Default configurationUnless user explicitly specifies auth. module instance to use we try to use default-auth or default (in that order). SMTP pipelineIf user didn't specified custom pipeline (no pipeline steps declarations in server block) and specified hostname - we use default pipeline that does the following:
Default modulesUnless overridden by a user, maddy adds default module that implements authentication, email storage and delivery target (perhaps go-sqlmail? :)). So you can then literally specify hostname and have it just work. |
SPF is gross (doesn't handle relaying). Maybe we should just drop it? We could add DMARC checks though. |
We need a collection of use-cases to check how well our design (and more importantly -- config structure) works for them. @xeoncross, @sapiens-sapide, any thoughts? |
Sure. |
It is relatively easy to use caddyfile lexer to parse config into tree structure: https://hastebin.com/ovozofiweh.go So I guess our problem with configuration format is solved. |
This implies to set a DNS record for DKIM. I'm not sure if it's good to sign outgoing messages by default if DKIM signature can't be verified by peers because record is missing. |
The user will need to setup a bunch of records anyway (MX, DMARC, MTA-STS, etc). I think @foxcpp's idea of an embeddable zone file could solve this issue. |
Not if maddy runs a DNS server itself. I've been looking at adding a crippled DNS to projects using different Go libraries and it seems pretty due-able. Simply set the domain DNS to point to the same box, then maddy only replies to requests for
I have little interest in a configuration file for the use-cases I mentioned above. I would like to use maddy programmatically wiring in pipelines on a per-project bases. Then again, I see maddy not as a simple MTA/MDA/MSA, but as a powerful library to add a full SMTP/IMAP server to other projects. The benefit is a single binary / process which also runs a HTTP server, slack bot, queue client, etc.. |
@xeoncross I guess "maddy as a library" is not going to be the top-priority use-case to support. At first, we want to make "simple MTA/MDA/MSA" but only then a generic IMAP/SMTP framework. |
If you want libraries, you can already use go-imap, go-smtp et al. Maddy could become a DNS server, but just generating a zone file as @foxcpp suggested is probably better. We could always think again about it if there are issues with this approach. |
Reasons are explained here: #15 (comment)
#15 (comment)
--
Original post:
Let's say I configured IMAP endpoint as follows (where first line creates IMAP backend):
Then I want to have SMTP endpoint that will deliver mail to same storage. How would I do this?
This approach creates another set of problems, because it now requires two separate backend/upstream objects to coordinate access to the same storage (think of IMAP unilateral updates).
Global variables? External IPC sockets? All this seems to be dirty solution.
What is we can create one "storage" object and associate it with multiple IMAP/SMTP endpoints?
This will transform "another set of problems" into just serialization of access to storage object. Which is easily solved by throwing some mutexes into it (or even without them, I haven't tested that but go-sqlmail backend object should be safe for concurrent use by multiple goroutines).
It also reduces resources usage (we will have only one SQLite "connection" page cache, for example)
Now I can imagine something like this:
What do you think?
The text was updated successfully, but these errors were encountered: