From 87e057b2d72fc643381037ed87bcd65a6516a158 Mon Sep 17 00:00:00 2001 From: itsscb Date: Thu, 4 Aug 2022 13:30:00 +0200 Subject: [PATCH] Initial Commit --- go.mod | 15 ++ go.sum | 16 ++ mailmonitor.go | 546 +++++++++++++++++++++++++++++++++++++++++++++++++ main.go | 5 + 4 files changed, 582 insertions(+) create mode 100644 go.mod create mode 100644 go.sum create mode 100644 mailmonitor.go create mode 100644 main.go diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..9582b9b --- /dev/null +++ b/go.mod @@ -0,0 +1,15 @@ +module autoprint + +go 1.18 + +require ( + github.com/emersion/go-imap v1.2.1 + github.com/emersion/go-message v0.15.0 + gopkg.in/yaml.v2 v2.4.0 +) + +require ( + github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 // indirect + github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 // indirect + golang.org/x/text v0.3.7 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..2c380a1 --- /dev/null +++ b/go.sum @@ -0,0 +1,16 @@ +github.com/emersion/go-imap v1.2.1 h1:+s9ZjMEjOB8NzZMVTM3cCenz2JrQIGGo5j1df19WjTA= +github.com/emersion/go-imap v1.2.1/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5NsmKFSejY= +github.com/emersion/go-message v0.15.0 h1:urgKGqt2JAc9NFJcgncQcohHdiYb803YTH9OQwHBHIY= +github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4= +github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ= +github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= +github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY= +github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/mailmonitor.go b/mailmonitor.go new file mode 100644 index 0000000..c189fc9 --- /dev/null +++ b/mailmonitor.go @@ -0,0 +1,546 @@ +package main + +import ( + "encoding/base64" + "errors" + "io" + "log" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + "time" + + imap "github.com/emersion/go-imap" + "github.com/emersion/go-imap/client" + "github.com/emersion/go-message/mail" + "gopkg.in/yaml.v2" +) + +const ( + layout string = "2006-01-02T15:04:05Z07:00" +) + +type Monitor struct { + IMAPUri string `yaml:"IMAPUri"` + Username string `yaml:"Username"` + Password string `yaml:"Password"` + SourceFolder string `yaml:"SourceFolder"` + DestinationFolder string `yaml:"DestinationFolder"` + TLS bool `yaml:"TLS"` + DebugLevel int64 `yaml:"DebugLevel"` + Client *client.Client + FetchSet *imap.SeqSet + ReadSet *imap.SeqSet + MessageChan chan *imap.Message + DoneChan chan error +} + +func NewMonitor(IMAPUri, Username, Password, SourceFolder, DestinationFolder string, TLS bool) (Monitor, error) { + m := Monitor{ + IMAPUri: IMAPUri, + Username: Username, + Password: Password, + TLS: TLS, + FetchSet: &imap.SeqSet{}, + ReadSet: &imap.SeqSet{}, + SourceFolder: SourceFolder, + DestinationFolder: DestinationFolder, + MessageChan: make(chan *imap.Message, 10), + DoneChan: make(chan error), + } + if TLS { + cl, err := client.DialTLS(m.IMAPUri, nil) + if err != nil { + return m, err + } + m.Client = cl + return m, nil + } else { + cl, err := client.Dial(m.IMAPUri) + if err != nil { + return m, err + } + err = cl.StartTLS( + nil, + ) + + if err != nil { + return m, err + } + m.Client = cl + return m, nil + } +} + +func NewMonitorFromFile(path string) (Monitor, error) { + m := Monitor{ + FetchSet: &imap.SeqSet{}, + ReadSet: &imap.SeqSet{}, + MessageChan: make(chan *imap.Message, 10), + DoneChan: make(chan error), + } + + if _, err := os.Stat(path); err != nil { + return m, errors.New("File does not exist\n") + } + + f, err := os.ReadFile(path) + if err != nil { + return m, errors.New("File can't be opened\n") + } + + err = yaml.Unmarshal(f, &m) + if err != nil { + return m, errors.New("File can't be read\n") + } + log.Printf("Accessing E-Mail-Account %s on %s and moving unread Messages from %s to %s", m.Username, m.IMAPUri, m.SourceFolder, m.DestinationFolder) + return m, nil +} + +func (m *Monitor) Login() error { + if m.DebugLevel > 0 { + log.Printf("Logging in as %s on %s. TLS: %t\n", m.Username, m.IMAPUri, m.TLS) + } + if m.TLS { + if m.DebugLevel > 0 { + log.Printf("Dialing TLS...\n") + } + cl, err := client.DialTLS(m.IMAPUri, nil) + if err != nil { + return err + } + m.Client = cl + } else { + if m.DebugLevel > 0 { + log.Printf("Dialing without TLS...\n") + } + cl, err := client.Dial(m.IMAPUri) + if err != nil { + return err + } + if m.DebugLevel > 0 { + log.Printf("Starting TLS...\n") + } + err = cl.StartTLS( + nil, + ) + if err != nil { + return err + } + m.Client = cl + } + if m.DebugLevel > 0 { + log.Printf("Sending Login Request...\n") + } + err := m.Client.Login(m.Username, m.Password) + if err != nil { + log.Printf("%s - Login failed: %s", m.Username, err) + } + if m.DebugLevel > 1 { + m.Client.SetDebug(os.Stderr) + } + log.Println("Logged in!") + return err +} + +func (m *Monitor) CheckConnection() error { + if m.DebugLevel > 0 { + log.Printf("Checking ConnectionState: %d\n", m.Client.State()) + } + if m.Client.State() != imap.ConnectedState && + m.Client.State() != imap.AuthenticatedState && + m.Client.State() != imap.SelectedState { + err := m.Login() + if err != nil { + log.Printf("%s - Login failed: %s", m.Username, err) + return err + } + } + return nil +} + +func (m *Monitor) Fetch() { + if m.DebugLevel > 0 { + log.Printf("Fetching unread Messages from Folder: %s\n", m.SourceFolder) + } + if err := m.CheckConnection(); err != nil { + log.Printf("%s - Connection lost: %s", m.Username, err) + return + } + + if m.DebugLevel > 0 { + log.Printf("Preparing Search Request by Selecting Folder: %s\n", m.SourceFolder) + } + criteria := imap.NewSearchCriteria() + criteria.WithoutFlags = []string{imap.SeenFlag} + _, err := m.Client.Select(m.SourceFolder, false) + if err != nil { + log.Println(err) + } + + if m.DebugLevel > 0 { + log.Printf("Sending Search Request...\n") + } + ids, err := m.Client.Search(criteria) + if err != nil { + log.Println(err) + } + + if m.DebugLevel > 0 { + log.Printf("Found %d Messages in %s\n", len(ids), m.SourceFolder) + } + if len(ids) <= 0 { + return + } + + m.FetchSet.AddNum(ids...) + + if m.DebugLevel > 0 { + log.Printf("Fetching %d Messages from %s\n", len(ids), m.SourceFolder) + } + go func() { + m.DoneChan <- m.Client.Fetch(m.FetchSet, []imap.FetchItem{"BODY.PEEK[]"}, m.MessageChan) //imap.FetchRFC822}, m.MessageChan) + }() + + err = m.PrintAll() + if err != nil { + log.Println(err) + } + log.Printf("Fetching done\n") +} + +func (m *Monitor) MarkRead() { + if m.DebugLevel > 0 { + log.Printf("Marking Messages read in Folder: %s\n", m.DestinationFolder) + } + + if err := m.CheckConnection(); err != nil { + log.Printf("%s - Connection lost: %s", m.Username, err) + return + } + + if m.DebugLevel > 0 { + log.Printf("Preparing Search Request by Selecting Folder: %s\n", m.DestinationFolder) + } + criteria := imap.NewSearchCriteria() + criteria.WithoutFlags = []string{imap.SeenFlag} + _, err := m.Client.Select(m.DestinationFolder, false) + if err != nil { + log.Println("Marking read failed: ", err) + } + + if m.DebugLevel > 0 { + log.Printf("Sending Search Request...\n") + } + ids, err := m.Client.Search(criteria) + if err != nil { + log.Println("Getting unread messages: ", err) + } + + if m.DebugLevel > 0 { + log.Printf("Found %d Messages in %s\n", len(ids), m.DestinationFolder) + } + if len(ids) <= 0 { + log.Println("No unread Messages found.") + return + } + + m.ReadSet.Clear() + m.ReadSet.AddNum(ids...) + + if m.DebugLevel > 0 { + log.Printf("Marking %d Messages read in %s\n", len(ids), m.DestinationFolder) + } + item := imap.FormatFlagsOp(imap.AddFlags, true) + flags := []interface{}{imap.SeenFlag} + err = m.Client.Store(m.ReadSet, item, flags, nil) + if err != nil { + log.Fatal("Marking as read: ", err) + } + if m.DebugLevel > 0 { + log.Printf("Marked %d Messages read in %s\n", len(ids), m.DestinationFolder) + } + log.Printf("Marking read done\n") +} + +func (m *Monitor) PrintAll() error { + moveSeqSet := new(imap.SeqSet) + if m.DebugLevel > 0 { + log.Printf("Downloading & sending fetched Messages to Default Printer\n") + } + + for msg := range m.MessageChan { + var file string + var err error + var subject string + var from string + var to string + var cc string + var date string + var header []string + var html int + var text int + + timestamp := time.Now().Format(layout) + + file = filepath.Join(os.TempDir(), (base64.StdEncoding.EncodeToString([]byte(timestamp)))) + + filehtml := file + ".html" + filetxt := file + ".txt" + + fhtml, err := os.Create(filehtml) + if err != nil { + log.Println("File Creation: ", err) + return err + } + defer fhtml.Close() + + ftxt, err := os.Create(filetxt) + if err != nil { + log.Println("File Creation: ", err) + return err + } + defer ftxt.Close() + + for _, literal := range msg.Body { + mr, err := mail.CreateReader(literal) + if err != nil { + log.Fatal("Creating Reader:", err) + return err + } + + fr, _ := mr.Header.AddressList("From") + for _, a := range fr { + from += a.Address + "," + } + from = from[:len(from)-1] + + t, _ := mr.Header.AddressList("To") + for _, a := range t { + to += a.Address + "," + } + to = to[:len(to)-1] + + c, _ := mr.Header.AddressList("Cc") + if len(c) > 0 { + for _, a := range c { + cc += a.Address + "," + } + cc = cc[:len(cc)-1] + } + date = mr.Header.Get("Date") + subject = mr.Header.Get("Subject") + + if m.DebugLevel > 0 { + log.Println(date, from, to, subject, cc) + } + + if "" != date { + header = append( + header, + ("Date:\t\t" + date + "\n"), + ) + } + if "" != from { + header = append( + header, + ("From:\t\t" + from + "\n"), + ) + } + if "" != to { + header = append( + header, + ("To:\t\t" + to + "\n"), + ) + } + if "" != cc { + header = append( + header, + ("Cc:\t\t" + cc + "\n"), + ) + } + if "" != subject { + header = append( + header, + ("Subject:\t" + subject + "\n"), + ) + } + + header = append( + header, + ("====================================\n\n\n\n"), + ) + + for _, s := range header { + fhtml.WriteString(s) + if err != nil { + log.Printf("Writing File (Header): %s\n", err) + } + ftxt.WriteString(s) + if err != nil { + log.Printf("Writing File (Header): %s\n", err) + } + } + for { + p, err := mr.NextPart() + if err == io.EOF { + break + } else if err != nil { + log.Printf("Reading Message: %s\n", err) + return err + } + + switch h := p.Header.(type) { + case *mail.InlineHeader: + + t, _, err := h.ContentType() + if err != nil { + log.Printf("Reading ContentType: %s\n", err) + return err + } + switch t { + case "text/html": + htmltmp := parseMessage(p.Body, fhtml) + if htmltmp > 0 { + html = htmltmp + } + case "text/plain": + texttmp := parseMessage(p.Body, ftxt) + if texttmp > 0 { + text = texttmp + } + default: + continue + } + } + } + moveSeqSet.AddNum(msg.Uid) + err = m.Client.Move(moveSeqSet, m.DestinationFolder) + if err != nil { + log.Println("Moving Item: ", err) + } + } + if text > 0 { + file = filetxt + } else if html > 0 { + file = filehtml + } + + err = printLinux(file, true) + if err != nil { + log.Println("Printing: ", err) + return err + } + } + + m.MessageChan = make(chan *imap.Message) + return nil +} + +func parseMessage(r io.Reader, f *os.File) int { + var err error + content, _ := io.ReadAll(r) + i, err := f.Write(content) + if err != nil { + log.Printf("Writing File (%s): %s", f.Name(), err) + } + return i +} + +func CheckPrerequisites() error { + var err error + _, err = exec.LookPath("wkhtmltopdf") + if err != nil { + log.Println(err) + return err + } + _, err = exec.LookPath("lp") + if err != nil { + log.Println(err) + return err + } + return err +} + +func printLinux(path string, delete bool) error { + lp, err := exec.LookPath("lp") + if err != nil { + log.Println(err) + return err + } + if strings.Contains(path, "html") { + npath := strings.ReplaceAll(path, ".html", ".pdf") + + wkhtmltopdf, err := exec.LookPath("wkhtmltopdf") + if err != nil { + log.Println(err) + return err + } + + cmd := exec.Command(wkhtmltopdf, path, npath) + _ = cmd.Run() + if delete { + err = os.Remove(path) + if err != nil { + log.Println(err) + } + } + path = npath + } + cmd := exec.Command(lp, path) + _ = cmd.Run() + + if delete { + err = os.Remove(path) + if err != nil { + log.Println(err) + } + } + + return err +} + +func Run() { + var m Monitor + var err error + + argLength := len(os.Args[1:]) + if err := CheckPrerequisites(); err != nil { + log.Fatal(err) + } + switch { + case argLength <= 0: + if _, err := os.Stat("settings.yaml"); err != nil { + log.Fatal("Required Arguments missing!") + } + m, err = NewMonitorFromFile("settings.yaml") + if err != nil { + log.Fatal(err) + } + + case argLength >= 1: + m, err = NewMonitorFromFile(os.Args[1]) + if err != nil { + log.Fatal(err) + } + + if argLength > 1 { + d, err := strconv.ParseInt(os.Args[2], 10, 64) + if err != nil { + log.Fatal(err) + } + m.DebugLevel = d + } + + default: + log.Fatal("Invalid or missing Parameters. Make sure to pass the pass to the 'settings.yaml'") + } + err = m.Login() + if err != nil { + log.Fatal(err) + } + m.Fetch() + m.MarkRead() +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..2a284f3 --- /dev/null +++ b/main.go @@ -0,0 +1,5 @@ +package main + +func main() { + Run() +}