commit 2ce834db4a4060362366f4d403ec70f5ad64a0b1 Author: Knut Ahlers Date: Sat Jun 27 15:25:16 2020 +0200 Initial version diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..619b562 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +automail +config.yaml +.env +store.yaml diff --git a/commands.go b/commands.go new file mode 100644 index 0000000..64196a9 --- /dev/null +++ b/commands.go @@ -0,0 +1,124 @@ +package main + +import ( + "encoding/json" + "io" + + "github.com/emersion/go-imap" + "github.com/emersion/go-imap/client" + "github.com/pkg/errors" +) + +type command interface { + Execute(*client.Client, *imap.Message, io.Writer) error +} + +type commandTypeWrap struct { + Type string `json:"type"` +} + +func (c commandTypeWrap) rewrap(data []byte) (command, error) { + var out command + + switch c.Type { + + case "move": + out = new(commandMove) + + case "add_flags": + out = new(commandAddFlags) + + case "del_flags": + out = new(commandDelFlags) + + case "set_flags": + out = new(commandSetFlags) + + default: + return nil, errors.New("Command not found") + + } + + return out, errors.Wrap(json.Unmarshal(data, out), "Unable to unmarshal into command") +} + +type commandMove struct { + ToMailbox string `json:"to_mailbox"` +} + +func (c commandMove) Execute(client *client.Client, msg *imap.Message, stdin io.Writer) error { + s := &imap.SeqSet{} + s.AddNum(msg.Uid) + + if err := client.UidCopy(s, c.ToMailbox); err != nil { + return errors.Wrap(err, "Unable to copy to target mailbox") + } + + return errors.Wrap( + client.UidStore(s, imap.FormatFlagsOp(imap.AddFlags, true), []interface{}{imap.DeletedFlag}, nil), + "Unable to set deleted flag in original mailbox", + ) +} + +type commandAddFlags struct { + Flags []string `json:"flags"` +} + +func (c commandAddFlags) Execute(client *client.Client, msg *imap.Message, stdin io.Writer) error { + var ( + flags []interface{} + s = &imap.SeqSet{} + ) + s.AddNum(msg.Uid) + + for _, f := range c.Flags { + flags = append(flags, f) + } + + return errors.Wrap( + client.UidStore(s, imap.FormatFlagsOp(imap.AddFlags, true), flags, nil), + "Unable to add flags", + ) +} + +type commandDelFlags struct { + Flags []string `json:"flags"` +} + +func (c commandDelFlags) Execute(client *client.Client, msg *imap.Message, stdin io.Writer) error { + var ( + flags []interface{} + s = &imap.SeqSet{} + ) + s.AddNum(msg.Uid) + + for _, f := range c.Flags { + flags = append(flags, f) + } + + return errors.Wrap( + client.UidStore(s, imap.FormatFlagsOp(imap.RemoveFlags, true), flags, nil), + "Unable to remove flags", + ) +} + +type commandSetFlags struct { + Flags []string `json:"flags"` +} + +func (c commandSetFlags) Execute(client *client.Client, msg *imap.Message, stdin io.Writer) error { + var ( + flags []interface{} + s = &imap.SeqSet{} + ) + s.AddNum(msg.Uid) + + for _, f := range c.Flags { + flags = append(flags, f) + } + + return errors.Wrap( + client.UidStore(s, imap.FormatFlagsOp(imap.SetFlags, true), flags, nil), + "Unable to set flags", + ) +} diff --git a/config.go b/config.go new file mode 100644 index 0000000..51a9c5b --- /dev/null +++ b/config.go @@ -0,0 +1,24 @@ +package main + +import ( + "os" + + "github.com/pkg/errors" + "gopkg.in/yaml.v2" +) + +type config struct { + Handlers []mailHandler `yaml:"handlers"` +} + +func loadConfig() (*config, error) { + var out = &config{} + + f, err := os.Open(cfg.Config) + if err != nil { + return nil, errors.Wrap(err, "Unable to open config file") + } + defer f.Close() + + return out, errors.Wrap(yaml.NewDecoder(f).Decode(out), "Unable to decode config") +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..0cf5a0e --- /dev/null +++ b/go.mod @@ -0,0 +1,14 @@ +module github.com/Luzifer/automail + +go 1.14 + +require ( + github.com/Luzifer/go_helpers/v2 v2.10.0 + github.com/Luzifer/rconfig/v2 v2.2.1 + github.com/emersion/go-imap v1.0.5 + github.com/influxdata/influxdb1-client v0.0.0-20200515024757-02f0bf5dbca3 + github.com/jhillyerd/enmime v0.8.1 + github.com/pkg/errors v0.9.1 + github.com/sirupsen/logrus v1.6.0 + gopkg.in/yaml.v2 v2.2.2 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d496291 --- /dev/null +++ b/go.sum @@ -0,0 +1,63 @@ +github.com/Luzifer/go_helpers v1.4.0 h1:Pmm058SbYewfnpP1CHda/zERoAqYoZFiBHF4l8k03Ko= +github.com/Luzifer/go_helpers/v2 v2.10.0 h1:rA3945P6tH1PKRdcVD+nAdAWojfgwX8wQm/jjUNPmfg= +github.com/Luzifer/go_helpers/v2 v2.10.0/go.mod h1:ZnWxPjyCdQ4rZP3kNiMSUW/7FigU1X9Rz8XopdJ5ZCU= +github.com/Luzifer/rconfig v1.2.0 h1:waD1sqasGVSQSrExpLrQ9Q1JmMaltrS391VdOjWXP/I= +github.com/Luzifer/rconfig/v2 v2.2.1 h1:zcDdLQlnlzwcBJ8E0WFzOkQE1pCMn3EbX0dFYkeTczg= +github.com/Luzifer/rconfig/v2 v2.2.1/go.mod h1:OKIX0/JRZrPJ/ZXXWklQEFXA6tBfWaljZbW37w+sqBw= +github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/ztvmWKFcI7UGb5/HQT7B+i3a2myKgI= +github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emersion/go-imap v1.0.5 h1:8xg/d2wo2BBP3AEP5AOaM/6i8887RGyVW2st/IVHWUw= +github.com/emersion/go-imap v1.0.5/go.mod h1:yKASt+C3ZiDAiCSssxg9caIckWF/JG7ZQTO7GAmvicU= +github.com/emersion/go-message v0.11.1/go.mod h1:C4jnca5HOTo4bGN9YdqNQM9sITuT3Y0K6bSUw9RklvY= +github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b h1:uhWtEWBHgop1rqEk2klKaxPAkVDCXexai6hSuRQ7Nvs= +github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k= +github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U= +github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/gogs/chardet v0.0.0-20150115103509-2404f7772561 h1:aBzukfDxQlCTVS0NBUjI5YA3iVeaZ9Tb5PxNrrIP1xs= +github.com/gogs/chardet v0.0.0-20150115103509-2404f7772561/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14= +github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf/go.mod h1:hyb9oH7vZsitZCiBt0ZvifOrB+qc8PS5IiilCIb87rg= +github.com/influxdata/influxdb1-client v0.0.0-20200515024757-02f0bf5dbca3 h1:k3/6a1Shi7GGCp9QpyYuXsMM6ncTOjCzOE9Fd6CDA+Q= +github.com/influxdata/influxdb1-client v0.0.0-20200515024757-02f0bf5dbca3/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= +github.com/jaytaylor/html2text v0.0.0-20190408195923-01ec452cbe43 h1:jTkyeF7NZ5oIr0ESmcrpiDgAfoidCBF4F5kJhjtaRwE= +github.com/jaytaylor/html2text v0.0.0-20190408195923-01ec452cbe43/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk= +github.com/jhillyerd/enmime v0.8.1 h1:Kz4xj3sJJ4Ju8e+w/7v9H4Matv5ijPgv7UkhPf+C15I= +github.com/jhillyerd/enmime v0.8.1/go.mod h1:MBHs3ugk03NGjMM6PuRynlKf+HA5eSillZ+TRCm73AE= +github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/leekchan/gtf v0.0.0-20190214083521-5fba33c5b00b/go.mod h1:thNruaSwydMhkQ8dXzapABF9Sc1Tz08ZBcDdgott9RA= +github.com/martinlindhe/base36 v1.0.0/go.mod h1:+AtEs8xrBpCeYgSLoY/aJ6Wf37jtBuR0s35750M27+8= +github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y= +github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/olekukonko/tablewriter v0.0.1 h1:b3iUnf1v+ppJiOfNX4yxxqfWKMQPZR5yoh8urCTFX88= +github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU= +github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo= +github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 h1:Ao/3l156eZf2AW5wK8a7/smtodRU+gha3+BeqJ69lRk= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/validator.v2 v2.0.0-20180514200540-135c24b11c19 h1:WB265cn5OpO+hK3pikC9hpP1zI/KTwmyMFKloW9eOVc= +gopkg.in/validator.v2 v2.0.0-20180514200540-135c24b11c19/go.mod h1:o4V0GXN9/CAmCsvJ0oXYZvrZOe7syiDZSN1GWGZTGzc= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/handler.go b/handler.go new file mode 100644 index 0000000..b1a35c1 --- /dev/null +++ b/handler.go @@ -0,0 +1,156 @@ +package main + +import ( + "bufio" + "encoding/json" + "os" + "os/exec" + "regexp" + "strings" + + "github.com/emersion/go-imap" + "github.com/emersion/go-imap/client" + "github.com/jhillyerd/enmime" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" +) + +type mailTransport struct { + Attachments []string `json:"attachments"` + Headers map[string]string `json:"headers"` + HTML string `json:"html"` + Text string `json:"text"` +} + +func mailToTransport(msg *enmime.Envelope) *mailTransport { + var out = &mailTransport{ + Headers: map[string]string{}, + HTML: msg.HTML, + Text: msg.Text, + } + + for _, a := range msg.Attachments { + out.Attachments = append(out.Attachments, a.FileName) + } + + for _, hn := range msg.GetHeaderKeys() { + out.Headers[hn] = msg.GetHeader(hn) + } + + return out +} + +type mailHandler struct { + Match []matcher `yaml:"match"` + Command []string `yaml:"command"` +} + +func (m mailHandler) Handles(msg *enmime.Envelope) bool { + for _, ma := range m.Match { + if ma.Match(msg) { + return true + } + } + return false +} + +func (m mailHandler) Process(imapClient *client.Client, msg *imap.Message, envelope *enmime.Envelope) error { + cmd := exec.Command(m.Command[0], m.Command[1:]...) + cmd.Stderr = os.Stderr + + stdin, err := cmd.StdinPipe() + if err != nil { + return errors.Wrap(err, "Unable to create stdin pipe") + } + defer stdin.Close() + + stdout, err := cmd.StdoutPipe() + if err != nil { + return errors.Wrap(err, "Unable to create stdout pipe") + } + defer stdout.Close() + + go func() { + scanner := bufio.NewScanner(stdout) + for scanner.Scan() { + if scanner.Err() != nil { + return + } + + var cw = new(commandTypeWrap) + if err := json.Unmarshal(scanner.Bytes(), cw); err != nil { + log.WithError(err).Error("Unable to unmarshal command") + continue + } + + c, err := cw.rewrap(scanner.Bytes()) + if err != nil { + log.WithError(err).Error("Unable to parse command") + continue + } + + if err = c.Execute(imapClient, msg, stdin); err != nil { + log.WithError(err).Error("Unable to execute command") + continue + } + } + }() + + if err = cmd.Start(); err != nil { + return errors.Wrap(err, "Unable to start process") + } + + if err = json.NewEncoder(stdin).Encode(mailToTransport(envelope)); err != nil { + return errors.Wrap(err, "Unable to send mail to process") + } + + return errors.Wrap(cmd.Wait(), "Process exited unclean") +} + +type matcher struct { + Any bool `yaml:"any"` + Header string `yaml:"header"` + Exact *string `yaml:"exact"` + Includes *string `yaml:"includes"` + RegExp *string `yaml:"regexp"` +} + +func (m matcher) Match(msg *enmime.Envelope) bool { + if m.Any { + return true + } + + switch strings.ToLower(m.Header) { + + case "cc": + return m.matchString(msg.GetHeader("cc")) + + case "from": + return m.matchString(msg.GetHeader("from")) + + case "subject": + return m.matchString(msg.GetHeader("subject")) + + case "to": + return m.matchString(msg.GetHeader("to")) + + } + + return false +} + +func (m matcher) matchString(s string) bool { + if m.Exact != nil && s == *m.Exact { + return true + } + + if m.Includes != nil && strings.Contains(s, *m.Includes) { + return true + } + + if m.RegExp != nil && regexp.MustCompile(*m.RegExp).MatchString(s) { + return true + } + + return false +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..ad6e70e --- /dev/null +++ b/main.go @@ -0,0 +1,178 @@ +package main + +import ( + "fmt" + "os" + "os/signal" + "syscall" + "time" + + "github.com/emersion/go-imap" + "github.com/emersion/go-imap/client" + "github.com/jhillyerd/enmime" + log "github.com/sirupsen/logrus" + + "github.com/Luzifer/rconfig/v2" +) + +var ( + cfg = struct { + Config string `flag:"config,c" default:"config.yaml" description:"Configuration file with instruction"` + FetchInterval time.Duration `flag:"interval,i" default:"1m" description:"Interval to fetch mails"` + IMAPHost string `flag:"imap-host,h" default:"" description:"Host of the IMAP server" validate:"nonzero"` + IMAPPort int `flag:"imap-port" default:"993" description:"Port of the IMAP server" validate:"nonzero"` + IMAPUser string `flag:"imap-user,u" default:"" description:"Username to access the IMAP server" validate:"nonzero"` + IMAPPass string `flag:"imap-pass,p" default:"" description:"Password to access the IMAP server" validate:"nonzero"` + LogLevel string `flag:"log-level" default:"info" description:"Log level (debug, info, warn, error, fatal)"` + Mailbox string `flag:"mailbox,m" default:"INBOX" description:"Mailbox to fetch from"` + StorageFile string `flag:"storage-file" default:"store.yaml" description:"Where to store persistent info"` + VersionAndExit bool `flag:"version" default:"false" description:"Prints current version and exits"` + }{} + + version = "dev" +) + +func init() { + rconfig.AutoEnv(true) + if err := rconfig.ParseAndValidate(&cfg); err != nil { + log.Fatalf("Unable to parse commandline options: %s", err) + } + + if cfg.VersionAndExit { + fmt.Printf("automail %s\n", version) + os.Exit(0) + } + + if l, err := log.ParseLevel(cfg.LogLevel); err != nil { + log.WithError(err).Fatal("Unable to parse log level") + } else { + log.SetLevel(l) + } +} + +func main() { + bodySection, err := imap.ParseBodySectionName("BODY[]") + if err != nil { + log.WithError(err).Fatal("Unable to parse body section") + } + + conf, err := loadConfig() + if err != nil { + log.WithError(err).Fatal("Unable to load config") + } + + store, err := loadStorage() + if err != nil { + log.WithError(err).Fatal("Unable to load storage file") + } + + imapClient, err := client.DialTLS(fmt.Sprintf("%s:%d", cfg.IMAPHost, cfg.IMAPPort), nil) + if err != nil { + log.WithError(err).Fatal("Unable to connect to IMAP server") + } + defer imapClient.Close() + + if err = imapClient.Login(cfg.IMAPUser, cfg.IMAPPass); err != nil { + log.WithError(err).Fatal("Unable to login to IMAP server") + } + + log.Info("IMAP connected and logged in") + + if _, err = imapClient.Select(cfg.Mailbox, false); err != nil { + log.WithError(err).Fatal("Unable to select mailbox") + } + + var ( + messages = make(chan *imap.Message, 1000) + sigs = make(chan os.Signal) + ticker = time.NewTicker(cfg.FetchInterval) + ) + signal.Notify(sigs, os.Interrupt, syscall.SIGTERM) + + for { + select { + + case <-ticker.C: + seq, err := imap.ParseSeqSet(fmt.Sprintf("%d:*", store.LastUID+1)) + if err != nil { + log.WithError(err).Error("Unable to parse sequence set") + continue + } + + ids, err := imapClient.UidSearch(&imap.SearchCriteria{ + Uid: seq, + }) + if err != nil { + log.WithError(err).Error("Unable to search for messages") + continue + } + + if len(ids) == 0 { + continue + } + + var tmpMsg = make(chan *imap.Message) + go func() { + for msg := range tmpMsg { + if msg.Uid <= store.LastUID { + continue + } + messages <- msg + } + }() + + fetchSeq := &imap.SeqSet{} + fetchSeq.AddNum(ids...) + + if err = imapClient.UidFetch(fetchSeq, []imap.FetchItem{ + imap.FetchFlags, + imap.FetchItem("BODY.PEEK[]"), + imap.FetchUid, + }, tmpMsg); err != nil { + log.WithError(err).Error("Unable to fetch messages") + continue + } + + case <-sigs: + return + + case msg := <-messages: + body := msg.GetBody(bodySection) + if body == nil { + log.WithField("uid", msg.Uid).Debug("Got message with nil body") + continue + } + + mail, err := enmime.ReadEnvelope(body) + if err != nil { + log.WithError(err).Error("Unable to parse message") + continue + } + + log.WithFields(log.Fields{ + "subject": mail.GetHeader("subject"), + "uid": msg.Uid, + }).Debug("Fetched message") + + // Check all handlers whether they want to handle the message + for _, hdl := range conf.Handlers { + if hdl.Handles(mail) { + go func(msg *imap.Message) { + if err := hdl.Process(imapClient, msg, mail); err != nil { + log.WithError(err).Error("Error while processing message") + } + }(msg) + } + } + + // Mark message as processed in store + if msg.Uid > store.LastUID { + store.LastUID = msg.Uid + if err = store.saveStorage(); err != nil { + log.WithError(err).Error("Unable to save storage") + } + } + + } + } +} diff --git a/store.go b/store.go new file mode 100644 index 0000000..188480b --- /dev/null +++ b/store.go @@ -0,0 +1,43 @@ +package main + +import ( + "os" + "path" + + "github.com/pkg/errors" + "gopkg.in/yaml.v2" +) + +type storage struct { + LastUID uint32 +} + +func loadStorage() (*storage, error) { + var out = &storage{} + + if _, err := os.Stat(cfg.StorageFile); os.IsNotExist(err) { + return out, nil + } + + f, err := os.Open(cfg.StorageFile) + if err != nil { + return nil, errors.Wrap(err, "Failed to open storage file") + } + defer f.Close() + + return out, errors.Wrap(yaml.NewDecoder(f).Decode(out), "Unable to decode storage file") +} + +func (s storage) saveStorage() error { + if err := os.MkdirAll(path.Dir(cfg.StorageFile), 0700); err != nil { + return errors.Wrap(err, "Unable to ensure directory for storage file") + } + + f, err := os.Create(cfg.StorageFile) + if err != nil { + return errors.Wrap(err, "Unable to create storage file") + } + defer f.Close() + + return errors.Wrap(yaml.NewEncoder(f).Encode(s), "Unable to encode storage file") +}