From 2ce834db4a4060362366f4d403ec70f5ad64a0b1 Mon Sep 17 00:00:00 2001 From: Knut Ahlers Date: Sat, 27 Jun 2020 15:25:16 +0200 Subject: [PATCH] Initial version --- .gitignore | 4 ++ commands.go | 124 ++++++++++++++++++++++++++++++++++++ config.go | 24 +++++++ go.mod | 14 +++++ go.sum | 63 +++++++++++++++++++ handler.go | 156 +++++++++++++++++++++++++++++++++++++++++++++ main.go | 178 ++++++++++++++++++++++++++++++++++++++++++++++++++++ store.go | 43 +++++++++++++ 8 files changed, 606 insertions(+) create mode 100644 .gitignore create mode 100644 commands.go create mode 100644 config.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 handler.go create mode 100644 main.go create mode 100644 store.go 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") +}