Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ go install github.com/ruhrcloud/rover/cmd/rover@latest

## Configuration
```yaml
debug: false
tasks:
- name: "johndoe"
from:
Expand Down Expand Up @@ -46,6 +45,7 @@ tasks:
Usage of rover:
-config string
path to the config file (default "rover.yml")
-verbose
```

## Legal
Expand Down
162 changes: 17 additions & 145 deletions cmd/rover/main.go
Original file line number Diff line number Diff line change
@@ -1,162 +1,34 @@
package main

import (
"bytes"
"encoding/json"
"context"
"flag"
"fmt"
"io"
"log"
"net/http"
"os"
"os/signal"
"syscall"

"github.com/emersion/go-imap"
"github.com/emersion/go-imap/client"
"github.com/emersion/go-message/mail"
"github.com/ruhrcloud/rover/internal/config"
"github.com/ruhrcloud/rover/internal/tasks"
)

type Imap struct {
Host string `json:"host"`
User string `json:"user"`
Pass string `json:"pass"`
}

type Webdav struct {
Baseurl string `json:"baseurl"`
User string `json:"user"`
Pass string `json:"pass"`
}

type Config struct {
Imap Imap `json:"imap"`
Webdav Webdav `json:"webdav"`
}

func main() {
var file string
flag.StringVar(&file, "config", "config.json", "path to the config file")
var cfgPath string
var verbose bool
flag.StringVar(&cfgPath, "config", "rover.yml", "config file")
flag.BoolVar(&verbose, "verbose", false, "verbose")
flag.Parse()

f, err := os.Open(file)
if err != nil {
log.Fatal("Error opening config:", err)
}
defer f.Close()

byt, err := io.ReadAll(f)
if err != nil {
log.Fatal("Error reading config:", err)
}

var conf Config
json.Unmarshal(byt, &conf)

client, err := client.DialTLS(conf.Imap.Host, nil)
if err != nil {
log.Fatal("Error connecting to IMAP server:", err)
}
defer client.Logout()

if err := client.Login(conf.Imap.User, conf.Imap.Pass); err != nil {
log.Fatal("IMAP login failed:", err)
}
log.Println("Logged in to IMAP server")

mbox, err := client.Select("INBOX", false)
cfg, err := config.Load(cfgPath)
if err != nil {
log.Fatal("Unable to select INBOX:", err)
log.Fatal(err)
}
log.Printf("Mailbox %s selected. Total messages: %d\n", mbox.Name, mbox.Messages)

criteria := imap.NewSearchCriteria()
criteria.WithoutFlags = []string{"\\Seen"}
uids, err := client.Search(criteria)
ctx, stop := signal.NotifyContext(context.Background(),
syscall.SIGINT, syscall.SIGTERM)
defer stop()
err = tasks.Run(ctx, cfg)
if err != nil {
log.Fatal("Search error:", err)
log.Fatal(err)
}
if len(uids) == 0 {
log.Println("No unread messages found.")
return
}
log.Println("Found message UIDs:", uids)

seqset := new(imap.SeqSet)
seqset.AddNum(uids...)
section := &imap.BodySectionName{}
messages := make(chan *imap.Message, 10)
done := make(chan error, 1)

go func() {
done <- client.Fetch(seqset, []imap.FetchItem{imap.FetchEnvelope, section.FetchItem()}, messages)
}()

for msg := range messages {
if msg.Envelope == nil {
continue
}
log.Printf("Processing message UID %d: %s\n", msg.Uid, msg.Envelope.Subject)

r := msg.GetBody(section)
if r == nil {
log.Println("No message body returned for UID", msg.Uid)
continue
}

mr, err := mail.CreateReader(r)
if err != nil {
log.Println("Failed to create mail reader for UID", msg.Uid, ":", err)
continue
}

for {
part, err := mr.NextPart()
if err == io.EOF {
break
}
if err != nil {
log.Println("Error reading part:", err)
break
}

if h, ok := part.Header.(*mail.AttachmentHeader); ok {
filename, _ := h.Filename()
log.Println("Found attachment:", filename)

buf := new(bytes.Buffer)
if _, err := io.Copy(buf, part.Body); err != nil {
log.Println("Error reading attachment:", err)
continue
}

uploadURL := fmt.Sprintf("%s%s", conf.Webdav.Baseurl, filename)
log.Println("Uploading to:", uploadURL)

req, err := http.NewRequest("PUT", uploadURL, bytes.NewReader(buf.Bytes()))
if err != nil {
log.Println("Error creating HTTP request:", err)
continue
}
req.SetBasicAuth(conf.Webdav.User, conf.Webdav.Pass)

resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Println("Error uploading attachment:", err)
continue
}
resp.Body.Close()

if resp.StatusCode >= 200 && resp.StatusCode < 300 {
log.Printf("Attachment %s uploaded successfully.\n", filename)
} else {
log.Printf("Failed to upload %s: %s\n", filename, resp.Status)
}
}
}
}

if err := <-done; err != nil {
log.Fatal("Fetch error:", err)
}

log.Println("Processing complete.")
}

4 changes: 4 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,9 @@ require (

require (
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect
github.com/emersion/go-webdav v0.6.0 // indirect
github.com/gosimple/slug v1.15.0 // indirect
github.com/gosimple/unidecode v1.0.1 // indirect
golang.org/x/net v0.43.0 // indirect
golang.org/x/text v0.28.0 // indirect
)
11 changes: 11 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
github.com/emersion/go-ical v0.0.0-20240127095438-fc1c9d8fb2b6/go.mod h1:BEksegNspIkjCQfmzWgsgbu6KdeJ/4LwUZs7DMBzjzw=
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/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4=
Expand All @@ -7,6 +8,14 @@ github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTe
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk=
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM=
github.com/emersion/go-webdav v0.6.0 h1:rbnBUEXvUM2Zk65Him13LwJOBY0ISltgqM5k6T5Lq4w=
github.com/emersion/go-webdav v0.6.0/go.mod h1:mI8iBx3RAODwX7PJJ7qzsKAKs/vY429YfS2/9wKnDbQ=
github.com/gosimple/slug v1.15.0 h1:wRZHsRrRcs6b0XnxMUBM6WK1U1Vg5B0R7VkIf1Xzobo=
github.com/gosimple/slug v1.15.0/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ=
github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o=
github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc=
github.com/teambition/rrule-go v1.8.2/go.mod h1:Ieq5AbrKGciP1V//Wq8ktsTXwSwJHDD5mD/wLBGl3p4=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
Expand All @@ -16,6 +25,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
Expand Down
100 changes: 100 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package config

import (
"errors"
"os"
"strings"
"time"

"gopkg.in/yaml.v3"
)

type FromConf struct {
Host string `yaml:"host"`
User string `yaml:"user"`
Pass string `yaml:"pass"`
Mailbox string `yaml:"mailbox"`
}

type ToConf struct {
BaseURL string `yaml:"base_url"`
User string `yaml:"user"`
Pass string `yaml:"pass"`
Auth string `yaml:"auth"`
Token string `yaml:"token"`
}

type FilterConf struct {
Recipients []string `yaml:"recipients"`
Seen *bool `yaml:"seen"`
Extensions []string `yaml:"extensions"`
}

type Task struct {
Name string `yaml:"name"`
From FromConf `yaml:"from"`
To ToConf `yaml:"to"`
Path string `yaml:"path"`
Tags []string `yaml:"tags"`
Filter FilterConf `yaml:"filter"`
Interval string `yaml:"interval"`
Format string `yaml:"format"`
MarkSeen bool `yaml:"mark_seen"`
}

type Config struct {
Tasks []Task `yaml:"tasks"`
}

func Load(p string) (*Config, error) {
b, err := os.ReadFile(p)
if err != nil {
return nil, err
}
var c Config
if err := yaml.Unmarshal(b, &c); err != nil {
return nil, err
}
for i := range c.Tasks {
if err := validateTask(c.Tasks[i]); err != nil {
return nil, err
}
}
return &c, nil
}

func validateTask(t Task) error {
if strings.TrimSpace(t.Name) == "" {
return errors.New("task.name required")
}

if t.From.User == "" || t.From.Pass == "" {
return errors.New("from.user/from.pass required")
}
if t.From.Mailbox == "" {
return errors.New("from.mailbox required")
}
if t.To.BaseURL == "" {
return errors.New("to.base_url required")
}
switch strings.ToLower(strings.TrimSpace(t.To.Auth)) {
case "", "basic":
if t.To.User == "" || t.To.Pass == "" {
return errors.New("to.user/to.pass required for basic auth")
}
case "bearer":
if t.To.Token == "" {
return errors.New("to.token required for bearer auth")
}
default:
return errors.New("to.auth must be basic or bearer")
}
if _, err := time.ParseDuration(t.Interval); err != nil {
return errors.New("interval must be a valid duration")
}
if strings.TrimSpace(t.Format) == "" {
return errors.New("format template required")
}
return nil
}

Loading