├── .gitignore ├── README.md └── maildir.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Maildir processor for the [Go-guerrilla](https://github.com/flashmob/go-guerrilla) package. 2 | 3 | Maildir is a popular email storage format. 4 | 5 | ## About 6 | 7 | This package is a _Processor_ for the Go-Guerrilla default backend implementation. Use for this in your project 8 | if you are using Go-Guerrilla as a package and you would like to add the ability to deliver emails to Maildir folders, using Go-Guerrilla's default backend. 9 | 10 | ## Usage 11 | 12 | Import `"github.com/flashmob/maildir-processor"` to your Go-guerrilla project. 13 | Assuming you have imported the go-guerrilla package already, and all dependencies. 14 | 15 | Then, when [using go-guerrilla as a package](https://github.com/flashmob/go-guerrilla/wiki/Using-as-a-package), do something like this 16 | 17 | ```go 18 | 19 | 20 | cfg := &AppConfig{ 21 | LogFile: "stderr", 22 | AllowedHosts: []string{"example.com"}, 23 | BackendConfig: backends.BackendConfig{ 24 | "save_process" : "HeadersParser|Debugger|MailDir", 25 | "validate_process": "MailDir", 26 | "maildir_user_map" : "test=-1:-1", 27 | "maildir_path" : "_test/Maildir", 28 | }, 29 | } 30 | d := Daemon{Config: cfg} 31 | d.AddProcessor("FastCGI", fastcgi_processor.Processor) 32 | 33 | d.Start() 34 | 35 | // .. keep the server busy.. 36 | 37 | ``` 38 | 39 | Note that here we've added MailDir to the end of the save_process config option, 40 | then used the d.AddProcessor api call to register it. Then configured other settings. 41 | 42 | See the configuration section for how to configure. 43 | 44 | 45 | ## Configuration 46 | 47 | The following values are required in your `backend_config` section of your JSON configuration file 48 | 49 | * `maildir_path` - string. maildir_path may contain a `[user]` placeholder. This will be substituted at run time 50 | eg `/home/[user]/Maildir` will get substituted to `/home/test/Maildir` for `test@example.com` 51 | * `maildir_user_map` - string. This is a string holding user to group/id mappings - in other words, the recipient table, 52 | each record separated by "," where records have the following format: `=:`
53 | Example: `"test=1002:2003,guerrilla=1001:1001"` . Use -1 for `` & `` if you want to ignore these, otherwise get these numbers from /etc/passwd 54 | 55 | Don't forget to add `MailDir` to the end of your `save_process` config option, eg: 56 | 57 | `"save_process": "HeadersParser|Debugger|Hasher|Header|MailDir",` 58 | 59 | also add `MailDir` to the end of your `validate_process` config option, eg: 60 | 61 | `"validate_process": "MailDir",` 62 | 63 | ## Example 64 | 65 | Take a look at [Maildiranasaurus](https://github.com/flashmob/maildiranasaurus) - an SMTP server that uses Go-Guerrilla as a 66 | package and adds Maildir delivery using this package. 67 | 68 | ## Credits 69 | 70 | This package depends on Simon Lipp's [Go MailDir](https://github.com/sloonz/go-maildir) package. 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /maildir.go: -------------------------------------------------------------------------------- 1 | package maildir_processor 2 | 3 | import ( 4 | "fmt" 5 | "github.com/flashmob/go-guerrilla/backends" 6 | "github.com/flashmob/go-guerrilla/mail" 7 | "github.com/flashmob/go-guerrilla/response" 8 | _ "github.com/sloonz/go-maildir" 9 | "github.com/flashmob/go-maildir" 10 | "os" 11 | "os/user" 12 | "strconv" 13 | "strings" 14 | "sync" 15 | ) 16 | 17 | const MailDirFilePerms = 0600 18 | 19 | type maildirConfig struct { 20 | // maildir_path may contain a [user] placeholder. This will be substituted at run time 21 | // eg /home/[user]/Maildir will get substituted to /home/test/Maildir for test@example.com 22 | Path string `json:"maildir_path"` 23 | // This is a string holding user to group/id mappings - in other words, the recipient table 24 | // Each record separated by "," 25 | // Records have the following format: =: 26 | // use -1 for & if you want to ignore these, otherwise get these numbers from /etc/passwd 27 | // Example: "test=1002:2003,guerrilla=1001:1001" 28 | UserMap string `json:"maildir_user_map"` 29 | } 30 | 31 | type MailDir struct { 32 | userMap map[string][]int 33 | dirs map[string]*maildir.Maildir 34 | config *maildirConfig 35 | } 36 | 37 | // check to see if we have configured 38 | func (m *MailDir) checkUsers(rcpt []mail.Address, mailDirs map[string]*maildir.Maildir) bool { 39 | for i := range rcpt { 40 | if _, ok := mailDirs[rcpt[i].User]; !ok { 41 | return false 42 | } 43 | } 44 | return true 45 | } 46 | 47 | var mdirMux sync.Mutex 48 | 49 | // initDirs creates the mail dir folders if they haven't been created already 50 | func (m *MailDir) initDirs() error { 51 | if m.dirs == nil { 52 | m.dirs = make(map[string]*maildir.Maildir, 0) 53 | } 54 | // initialize some maildirs 55 | mdirMux.Lock() 56 | defer mdirMux.Unlock() 57 | for str, ids := range m.userMap { 58 | path := strings.Replace(m.config.Path, "[user]", str, 1) 59 | if mdir, err := maildir.NewWithPerm(path, true, MailDirFilePerms, ids[0], ids[1]); err == nil { 60 | m.dirs[str] = mdir 61 | } else { 62 | backends.Log().WithError(err).Error("could not create Maildir. Please check the config") 63 | return err 64 | } 65 | } 66 | return nil 67 | } 68 | 69 | func (m *MailDir) validateRcpt(addr *mail.Address) backends.RcptError { 70 | u := strings.ToLower(addr.User) 71 | mdir, ok := m.dirs[u] 72 | if !ok { 73 | return backends.NoSuchUser 74 | } 75 | if _, err := os.Stat(mdir.Path); err != nil { 76 | return backends.StorageNotAvailable 77 | } 78 | return nil 79 | } 80 | 81 | func newMailDir(config *maildirConfig) (*MailDir, error) { 82 | m := &MailDir{} 83 | m.config = config 84 | m.userMap = usermap(m.config.UserMap) 85 | if strings.Index(m.config.Path, "~/") == 0 { 86 | // expand the ~/ to home dir 87 | usr, err := user.Current() 88 | if err != nil { 89 | backends.Log().WithError(err).Error("could not expand ~/ to homedir") 90 | return nil, err 91 | } 92 | m.config.Path = usr.HomeDir + m.config.Path[1:] 93 | } 94 | if err := m.initDirs(); err != nil { 95 | return nil, err 96 | } 97 | return m, nil 98 | } 99 | 100 | // usermap parses the usermap config strings and returns the result in a map 101 | // Example: "test=1002:2003,guerrilla=1001:1001" 102 | // test and guerrilla are usernames 103 | // number 1002 is the uid, 2003 is gid 104 | func usermap(usermap string) (ret map[string][]int) { 105 | ret = make(map[string][]int, 0) 106 | users := strings.Split(usermap, ",") 107 | for i := range users { 108 | u := strings.Split(users[i], "=") 109 | if len(u) != 2 { 110 | return 111 | } 112 | ids := strings.Split(u[1], ":") 113 | if len(ids) != 2 { 114 | return 115 | } 116 | n := make([]int, 0) 117 | ret[u[0]] = n 118 | for k := range ids { 119 | s, _ := strconv.Atoi(ids[k]) 120 | ret[u[0]] = append(ret[u[0]], s) 121 | } 122 | } 123 | return 124 | } 125 | 126 | var Processor = func() backends.Decorator { 127 | 128 | // The following initialization is run when the program first starts 129 | 130 | // config will be populated by the initFunc 131 | var ( 132 | m *MailDir 133 | ) 134 | // initFunc is an initializer function which is called when our processor gets created. 135 | // It gets called for every worker 136 | initializer := backends.InitializeWith(func(backendConfig backends.BackendConfig) error { 137 | configType := backends.BaseConfig(&maildirConfig{}) 138 | bcfg, err := backends.Svc.ExtractConfig(backendConfig, configType) 139 | 140 | if err != nil { 141 | return err 142 | } 143 | c := bcfg.(*maildirConfig) 144 | m, err = newMailDir(c) 145 | if err != nil { 146 | return err 147 | } 148 | return nil 149 | }) 150 | // register our initializer 151 | backends.Svc.AddInitializer(initializer) 152 | 153 | return func(c backends.Processor) backends.Processor { 154 | // The function will be called on each email transaction. 155 | // On success, it forwards to the next step in the processor call-stack, 156 | // or returns with an error if failed 157 | return backends.ProcessWith(func(e *mail.Envelope, task backends.SelectTask) (backends.Result, error) { 158 | if task == backends.TaskValidateRcpt { 159 | // Check the recipients for each RCPT command. 160 | // This is called each time a recipient is added, 161 | // validate only the _last_ recipient that was appended 162 | if size := len(e.RcptTo); size > 0 { 163 | if err := m.validateRcpt(&e.RcptTo[size-1]); err != nil { 164 | backends.Log().WithError(backends.NoSuchUser).Info("recipient not configured: ", e.RcptTo[size-1].User) 165 | return backends.NewResult( 166 | response.Canned.FailRcptCmd), 167 | backends.NoSuchUser 168 | } 169 | 170 | } 171 | return c.Process(e, task) 172 | } else if task == backends.TaskSaveMail { 173 | for i := range e.RcptTo { 174 | u := strings.ToLower(e.RcptTo[i].User) 175 | mdir, ok := m.dirs[u] 176 | if !ok { 177 | // no such user 178 | continue 179 | } 180 | if filename, err := mdir.CreateMail(e.NewReader()); err != nil { 181 | backends.Log().WithError(err).Error("Could not save email") 182 | return backends.NewResult(fmt.Sprintf("554 Error: could not save email for [%s]", u)), err 183 | } else { 184 | backends.Log().Debug("saved email as", filename) 185 | } 186 | } 187 | // continue to the next Processor in the decorator chain 188 | return c.Process(e, task) 189 | } else { 190 | return c.Process(e, task) 191 | } 192 | 193 | }) 194 | } 195 | } 196 | --------------------------------------------------------------------------------