forked from emersion/go-milter
-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2 from d--j/mailfilter
feat: introduce mail filter abstraction
- Loading branch information
Showing
27 changed files
with
3,927 additions
and
122 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,81 +6,81 @@ | |
|
||
A Go library to write mail filters. | ||
|
||
With this library you can write both the client (MTA/SMTP-Server) and server (milter filter) | ||
in pure Go without sendmail's libmilter. | ||
|
||
## Features | ||
|
||
* With this library you can write both the client (MTA/SMTP-Server) and server (milter filter) | ||
in pure Go without sendmail's libmilter. | ||
* Easy wrapper of the milter protocol that abstracts away many milter protocol quirks | ||
and lets you write mail filters with little effort. | ||
* UTF-8 support | ||
* IDNA support | ||
* Client & Server support milter protocol version 6 with all features. E.g.: | ||
* all milter events including DATA, UNKNOWN, ABORT and QUIT NEW CONNECTION | ||
* milter can skip e.g. body chunks when it does not need all chunks | ||
* milter can send progress notifications when response can take some time | ||
* milter can automatically instruct the MTA which macros it needs. | ||
* UTF-8 support | ||
|
||
## Installation | ||
|
||
```shell | ||
go get -u github.com/d--j/go-milter | ||
``` | ||
|
||
## Usage | ||
|
||
The following example is a milter filter that adds `[⚠️EXTERNAL] ` to the subject of all messages of unauthenticated users. | ||
|
||
See [GoDoc](https://godoc.org/github.com/d--j/go-milter/mailfilter) for more documentation and an example for a milter client or a raw milter server. | ||
|
||
```go | ||
package main | ||
|
||
import ( | ||
"log" | ||
"net" | ||
"sync" | ||
"context" | ||
"flag" | ||
"log" | ||
"strings" | ||
|
||
"github.com/d--j/go-milter" | ||
"github.com/d--j/go-milter/mailfilter" | ||
) | ||
|
||
type ExampleBackend struct { | ||
milter.NoOpMilter | ||
} | ||
|
||
func (b *ExampleBackend) RcptTo(rcptTo string, esmtpArgs string, m *milter.Modifier) (*milter.Response, error) { | ||
// reject the mail when it goes to [email protected] and is a local delivery | ||
if rcptTo == "[email protected]" && m.Macros.Get(milter.MacroRcptMailer) == "local" { | ||
return milter.RejectWithCodeAndReason(550, "We do not like you\r\nvery much, please go away") | ||
} | ||
return milter.RespContinue, nil | ||
} | ||
|
||
func main() { | ||
// create socket to listen on | ||
socket, err := net.Listen("tcp4", "127.0.0.1:6785") | ||
if err != nil { | ||
log.Fatal(err) | ||
} | ||
defer socket.Close() | ||
|
||
// define the backend, required actions, protocol options and macros we want | ||
server := milter.NewServer( | ||
milter.WithMilter(func() milter.Milter { | ||
return &ExampleBackend{} | ||
}), | ||
milter.WithProtocol(milter.OptNoConnect|milter.OptNoHelo|milter.OptNoMailFrom|milter.OptNoBody|milter.OptNoHeaders|milter.OptNoEOH|milter.OptNoUnknown|milter.OptNoData), | ||
milter.WithAction(milter.OptChangeFrom|milter.OptAddRcpt|milter.OptRemoveRcpt), | ||
milter.WithMaroRequest(milter.StageRcpt, []milter.MacroName{milter.MacroRcptMailer}), | ||
) | ||
defer server.Close() | ||
|
||
// start the milter | ||
var wgDone sync.WaitGroup | ||
wgDone.Add(1) | ||
go func(socket net.Listener) { | ||
if err := server.Serve(socket); err != nil { | ||
log.Fatal(err) | ||
} | ||
wgDone.Done() | ||
}(socket) | ||
|
||
log.Printf("Started milter on %s:%s", socket.Addr().Network(), socket.Addr().String()) | ||
|
||
// quit when milter quits | ||
wgDone.Wait() | ||
// parse commandline arguments | ||
var protocol, address string | ||
flag.StringVar(&protocol, "proto", "tcp", "Protocol family (unix or tcp)") | ||
flag.StringVar(&address, "addr", "127.0.0.1:10003", "Bind to address or unix domain socket") | ||
flag.Parse() | ||
|
||
// create and start the mail filter | ||
mailFilter, err := mailfilter.New(protocol, address, | ||
func(_ context.Context, trx *mailfilter.Transaction) (mailfilter.Decision, error) { | ||
// Reject message when it was sent to our SPAM trap | ||
if trx.HasRcptTo("spam-trap@スパム.example.com") { | ||
return mailfilter.CustomErrorResponse(550, "5.7.1 No thank you"), nil | ||
} | ||
// Prefix subject with [⚠️EXTERNAL] when user is not logged in | ||
if trx.MailFrom.AuthenticatedUser() == "" { | ||
subject, _ := trx.Headers.Subject() | ||
if !strings.HasPrefix(subject, "[⚠️EXTERNAL] ") { | ||
subject = "[⚠️EXTERNAL] " + subject | ||
} | ||
trx.Headers.SetSubject(subject) | ||
} | ||
return mailfilter.Accept, nil | ||
}, | ||
// optimization: we do not need the body of the message for our decision | ||
mailfilter.WithoutBody(), | ||
) | ||
if err != nil { | ||
log.Fatal(err) | ||
} | ||
log.Printf("Started milter on %s:%s", mailFilter.Addr().Network(), mailFilter.Addr().String()) | ||
|
||
// wait for the mail filter to end | ||
mailFilter.Wait() | ||
} | ||
``` | ||
|
||
See [![GoDoc](https://godoc.org/github.com/d--j/go-milter?status.svg)](https://godoc.org/github.com/d--j/go-milter) for more documentation and an example for a milter client. | ||
|
||
## License | ||
|
||
BSD 2-Clause | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
package mailfilter | ||
|
||
import ( | ||
"strings" | ||
|
||
"golang.org/x/net/idna" | ||
) | ||
|
||
// split an user@domain address into user and domain. | ||
// Includes the input address as third array element to quickly check if splitting must be re-done | ||
func split(addr string) []string { | ||
at := strings.LastIndex(addr, "@") | ||
if at < 0 { | ||
return []string{addr, "", addr} | ||
} | ||
|
||
return []string{addr[:at], addr[at+1:], addr} | ||
} | ||
|
||
type addr struct { | ||
Addr string | ||
Args string | ||
parts []string | ||
asciiDomain string | ||
unicodeDomain string | ||
} | ||
|
||
func (a *addr) initParts() { | ||
if len(a.parts) != 3 || a.parts[2] != a.Addr { | ||
a.parts = split(a.Addr) | ||
a.asciiDomain = "" | ||
a.unicodeDomain = "" | ||
} | ||
} | ||
|
||
func (a *addr) Local() string { | ||
a.initParts() | ||
return a.parts[0] | ||
} | ||
|
||
func (a *addr) Domain() string { | ||
a.initParts() | ||
return a.parts[1] | ||
} | ||
|
||
func (a *addr) AsciiDomain() string { | ||
domain := a.Domain() | ||
if domain == "" { | ||
return "" | ||
} | ||
if a.asciiDomain != "" { | ||
return a.asciiDomain | ||
} | ||
|
||
asciiDomain, err := idna.Lookup.ToASCII(domain) | ||
if err != nil { | ||
a.asciiDomain = domain | ||
return domain | ||
} | ||
a.asciiDomain = asciiDomain | ||
return asciiDomain | ||
} | ||
|
||
func (a *addr) UnicodeDomain() string { | ||
domain := a.Domain() | ||
if domain == "" { | ||
return "" | ||
} | ||
if a.unicodeDomain != "" { | ||
return a.unicodeDomain | ||
} | ||
|
||
unicodeDomain, err := idna.Lookup.ToUnicode(domain) | ||
if err != nil { | ||
a.unicodeDomain = domain | ||
return domain | ||
} | ||
a.unicodeDomain = unicodeDomain | ||
return unicodeDomain | ||
} | ||
|
||
type MailFrom struct { | ||
addr | ||
transport string | ||
authenticatedUser string | ||
authenticationMethod string | ||
} | ||
|
||
func (m *MailFrom) Transport() string { | ||
return m.transport | ||
} | ||
|
||
func (m *MailFrom) AuthenticatedUser() string { | ||
return m.authenticatedUser | ||
} | ||
|
||
func (m *MailFrom) AuthenticationMethod() string { | ||
return m.authenticationMethod | ||
} | ||
|
||
type RcptTo struct { | ||
addr | ||
transport string | ||
} | ||
|
||
func (r *RcptTo) Transport() string { | ||
return r.transport | ||
} | ||
|
||
func calculateRcptToDiff(orig []RcptTo, changed []RcptTo) (deletions []RcptTo, additions []RcptTo) { | ||
foundOrig := make(map[string]*RcptTo) | ||
foundChanged := make(map[string]bool) | ||
for _, r := range orig { | ||
foundOrig[r.Addr] = &r | ||
} | ||
for _, r := range changed { | ||
if o := foundOrig[r.Addr]; o == nil && !foundChanged[r.Addr] { | ||
additions = append(additions, r) | ||
} else if o != nil && o.Args != r.Args && !foundChanged[r.Addr] { | ||
deletions = append(deletions, *o) | ||
additions = append(additions, r) | ||
} | ||
foundChanged[r.Addr] = true | ||
} | ||
for _, r := range orig { | ||
if !foundChanged[r.Addr] { | ||
deletions = append(deletions, r) | ||
} | ||
} | ||
return | ||
} |
Oops, something went wrong.