From e9676f813603d868e1b35670e044af79b024e669 Mon Sep 17 00:00:00 2001 From: Laurent Ulrich Date: Tue, 29 Jul 2025 20:40:01 +0200 Subject: [PATCH] =?UTF-8?q?Ok=20pour=20la=20lecture=20des=20mails,=20reste?= =?UTF-8?q?=20=C3=A0=20voir=20comment=20les=20servir=20via=20http?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- blogentry.go | 9 ++++ cli.go | 49 ++++++++++++++++++ configuration.go | 14 +++-- imap_handler.go | 129 ++++++++++++++++++++++++++++++++++------------- mailblog.go | 119 ++++++++++++++++++++++++++++++++++++++----- 5 files changed, 265 insertions(+), 55 deletions(-) create mode 100644 blogentry.go create mode 100644 cli.go diff --git a/blogentry.go b/blogentry.go new file mode 100644 index 0000000..748b581 --- /dev/null +++ b/blogentry.go @@ -0,0 +1,9 @@ +package main + +type BlogEntry struct { + Id string + Title string + Author string + HTML string + Text string +} diff --git a/cli.go b/cli.go new file mode 100644 index 0000000..e15ead7 --- /dev/null +++ b/cli.go @@ -0,0 +1,49 @@ +package main + +import ( + "bufio" + "fmt" + "os" + "strings" +) + +func prompt(question string, options []string, defaultAnswer string, currentValue string, permitEmpty bool) (string, error) { + r := bufio.NewReader(os.Stdin) + if len(currentValue) == 0 { + currentValue = "None" + } + if len(defaultAnswer) == 0 { + defaultAnswer = "None" + } + if options == nil { + fmt.Print("->", question, "( defaults to:", defaultAnswer, " current value:", currentValue, " )", ":") + + } else { + fmt.Print("->", question, "( defaults to:", defaultAnswer, " current value:", currentValue, " )[ ", strings.Join(options, " | "), " ]", ":") + } + response, _ := r.ReadString('\n') + response = strings.Trim(response, "\n") + if len(response) == 0 && currentValue != "None" { + response = currentValue + } + if len(response) == 0 && defaultAnswer != "None" { + response = defaultAnswer + } + if options != nil { + validOption := false + for _, opt := range options { + if strings.EqualFold(response, opt) { + validOption = true + response = opt + break + } + } + if !validOption { + return response, fmt.Errorf("invalid option") + } + } + if !permitEmpty && len(response) == 0 { + return response, fmt.Errorf("empty response") + } + return response, nil +} diff --git a/configuration.go b/configuration.go index b757b15..2dfd0a9 100644 --- a/configuration.go +++ b/configuration.go @@ -2,17 +2,15 @@ package main type MailBoxConfiguration struct { Server string - Port int + Port string User string Password string SSL string + InBox string } type BlogConfiguration struct { - Title string - MailBox string - WWWRoot string -} -type Configuration struct { - MailBoxes map[string]MailBoxConfiguration - Blogs map[string]BlogConfiguration + Title string + ShortName string + MailBox MailBoxConfiguration + WWWRoot string } diff --git a/imap_handler.go b/imap_handler.go index 5868ccb..74c01e6 100644 --- a/imap_handler.go +++ b/imap_handler.go @@ -6,6 +6,8 @@ import ( "io" "log" "mime" + "strconv" + "strings" "github.com/emersion/go-imap/v2" "github.com/emersion/go-imap/v2/imapclient" @@ -15,12 +17,28 @@ import ( type MailBox struct { Server string + Port uint16 User string Password string + SSL string InBox string Client *imapclient.Client } +func (mb *MailBox) Configure(conf *MailBoxConfiguration) error { + + mb.Server = conf.Server + port, err := strconv.ParseUint(conf.Port, 10, 16) + if err != nil { + return err + } + mb.Port = uint16(port) + mb.User = conf.User + mb.Password = conf.Password + mb.SSL = conf.SSL + mb.InBox = conf.InBox + return nil +} func (mb *MailBox) ListMessages() (string, error) { return "", nil } @@ -30,9 +48,19 @@ func (mb *MailBox) Connect() error { WordDecoder: &mime.WordDecoder{CharsetReader: charset.Reader}, } var err error - mb.Client, err = imapclient.DialTLS(mb.Server, options) + imapServer := fmt.Sprintf("%s:%d", mb.Server, mb.Port) + switch mb.SSL { + case "SSL/TLS": + mb.Client, err = imapclient.DialTLS(imapServer, options) + case "StartTLS": + mb.Client, err = imapclient.DialStartTLS(imapServer, options) + case "NoTLS": + mb.Client, err = imapclient.DialInsecure(imapServer, options) + default: + return fmt.Errorf("bad tls configuration") + } if err != nil { - log.Println("Error connnecting to", mb.Server, ":", err) + log.Println("Error connnecting to", imapServer, ":", err) return err } log.Println("Connected") @@ -41,21 +69,33 @@ func (mb *MailBox) Connect() error { log.Fatal("failed to login:", err) } log.Println("Logged in") - + return nil +} +func (mb *MailBox) ListFolders() ([]string, error) { + folders := make([]string, 0) mailBoxes, err := mb.Client.List("", "%", nil).Collect() if err != nil { - log.Fatal("Error listing mailboxes", err) + return nil, err } for _, mbox := range mailBoxes { - log.Println("mailbox:", mbox.Mailbox) + folders = append(folders, mbox.Mailbox) } + return folders, nil +} +func (mb *MailBox) GetMessages() ([]BlogEntry, error) { + inbox, err := mb.Client.Select(mb.InBox, nil).Wait() if err != nil { log.Fatal("Error selecting mailbox:", mb.InBox, err) } log.Println("Inbox has", inbox.NumMessages, "messages") - for i := uint32(0); i <= inbox.NumMessages; i++ { + if inbox.NumMessages <= 0 { + return nil, fmt.Errorf("nomessages") + } + entries := make([]BlogEntry, 0) + for i := uint32(1); i <= inbox.NumMessages; i++ { + var entry BlogEntry seqSet := imap.SeqSetNum(i) bodySection := &imap.FetchItemBodySection{} fetchOptions := &imap.FetchOptions{ @@ -66,44 +106,63 @@ func (mb *MailBox) Connect() error { if err != nil { log.Fatal("Error fetching mails:", err) } - for _, msg := range messages { - log.Println("**************MESSAGE**************") - log.Println("From:", msg.Envelope.From) - log.Println("To:", msg.Envelope.To) - log.Println("Date:", msg.Envelope.Date) - log.Println("Subject:", msg.Envelope.Subject) + entry.Title = strings.Trim(messages[0].Envelope.Subject, "\t ") + if !strings.HasPrefix(strings.ToLower(entry.Title), "blog ") { + continue + } - section := msg.FindBodySection(bodySection) - ioReader := bytes.NewReader(section) - mailReader, err := mail.CreateReader(ioReader) - if err != nil { - log.Fatalf("failed to create mail reader: %v", err) + entry.Id = messages[0].Envelope.MessageID + entry.Title = entry.Title[5:] + entry.Author = messages[0].Envelope.From[0].Name + + section := messages[0].FindBodySection(bodySection) + ioReader := bytes.NewReader(section) + mailReader, err := mail.CreateReader(ioReader) + if err != nil { + return nil, err + } + for { + part, err := mailReader.NextPart() + if err == io.EOF { + break + } else if err != nil { + return nil, err } - for { - part, err := mailReader.NextPart() - if err == io.EOF { - break - } else if err != nil { - log.Fatal("Error reading part", err) - } - log.Println("------------PART-----------") - fmt.Println("Content-Type:", part.Header.Get("Content-Type")) - fmt.Println("Content-Transfer-Encoding:", part.Header.Get("Content-Transfer-Encoding")) - fmt.Println("header:", part.Header) - switch header := part.Header.(type) { - case *mail.AttachmentHeader: + + switch part.Header.(type) { + case *mail.AttachmentHeader: + /* filename, _ := header.Filename() + log.Println("------------FILE-----------") log.Println("Attachment:", filename) - case *mail.InlineHeader: - log.Println("------------INLINE-----------") - body, _ := io.ReadAll(part.Body) - log.Println(string(body)) + */ + case *mail.InlineHeader: + + body, _ := io.ReadAll(part.Body) + contentTypeFull := strings.TrimPrefix( + strings.ToLower(part.Header.Get("Content-Type")), "content-type") + mediaType, _, err := mime.ParseMediaType(strings.TrimLeft(contentTypeFull, ": ")) + if err != nil { + return nil, err } + switch mediaType { + case "text/plain": + entry.Text = string(body) + case "text/html": + entry.HTML = string(body) + default: + log.Println("Content-Type:", part.Header.Get("Content-Type")) + + } + + default: + log.Printf("Unkown part type %+v", part) } } + entries = append(entries, entry) } - return nil + return entries, nil } func (mb *MailBox) Close() { diff --git a/mailblog.go b/mailblog.go index 484ecfc..ccb9e3d 100644 --- a/mailblog.go +++ b/mailblog.go @@ -1,11 +1,13 @@ package main import ( + "encoding/json" + "fmt" "log" "net/http" - "net/url" "os" - "strings" + "regexp" + "strconv" ) func slash(w http.ResponseWriter, r *http.Request) { @@ -14,29 +16,122 @@ func slash(w http.ResponseWriter, r *http.Request) { func main() { - var mb MailBox + var err error + var configuration BlogConfiguration + args := os.Args[1:] - url, err := url.Parse(os.Getenv("MAILBOX")) + home := os.Getenv("HOME") + err = os.MkdirAll(fmt.Sprintf("%s/.config/", home), 0600) if err != nil { - log.Fatal("Bad parameter MAILBOX:", os.Getenv("MAILBOX"), "(", err, ")") + log.Fatal(err) } - mb.Server = url.Host - mb.User = os.Getenv("IMAP_USER") - mb.Password = os.Getenv("IMAP_PASSWORD") - mb.InBox, _ = strings.CutPrefix(url.Path, "/") + file, err := os.Open(fmt.Sprintf("%s/.config/mailblog.json", home)) + if err == nil { + defer file.Close() + decoder := json.NewDecoder(file) + err = decoder.Decode(&configuration) + if err != nil { + log.Fatal(err) + } + } + + if len(args) > 0 { + switch args[0] { + case "configure": + + configuration.Title, err = prompt("Blog Title", nil, "", configuration.Title, false) + if err != nil { + log.Fatal(err) + } + configuration.ShortName, err = prompt("Blog Short Name (only letters and numbers, no special signes)", nil, "blog", configuration.ShortName, false) + if err != nil { + log.Fatal(err) + } + if !regexp.MustCompile(`^[a-zA-Z0-9]+$`).MatchString(configuration.ShortName) { + log.Fatal("Short Name invalid") + } + + configuration.WWWRoot, err = prompt("Web Root directory", nil, fmt.Sprintf("/var/www/html/%s", configuration.ShortName), configuration.WWWRoot, false) + if err != nil { + log.Fatal(err) + } + configuration.MailBox.Server, err = prompt("IMAP Server Hostname or IP", nil, "", configuration.MailBox.Server, false) + if err != nil { + log.Fatal(err) + } + + configuration.MailBox.Port, err = prompt("IMAP Server Port", nil, "993", configuration.MailBox.Port, false) + if err != nil { + log.Fatal(err) + } + _, err = strconv.ParseUint(configuration.MailBox.Port, 10, 16) + if err != nil { + log.Fatal("IMAP Server Port invalid") + } + configuration.MailBox.User, err = prompt("IMAP User Login", nil, "", configuration.MailBox.User, false) + if err != nil { + log.Fatal(err) + } + configuration.MailBox.Password, err = prompt("IMAP User Password", nil, "", configuration.MailBox.Password, false) + if err != nil { + log.Fatal(err) + } + + options := [...]string{"SSL/TLS", "StartTLS", "NoTLS"} + configuration.MailBox.SSL, err = prompt("IMAP SSL/TLS", options[:], "SSL/TLS", configuration.MailBox.SSL, false) + if err != nil { + log.Fatal(err) + } + + configuration.MailBox.InBox, err = prompt("IMAP INBOX", nil, "INBOX", configuration.MailBox.InBox, false) + if err != nil { + log.Fatal(err) + } + + file, err = os.Create(fmt.Sprintf("%s/.config/mailblog.json", home)) + if err != nil { + log.Fatal(err) + } + encoder := json.NewEncoder(file) + err = encoder.Encode(configuration) + if err != nil { + log.Fatal(err) + } + fmt.Printf("Configuration save in %s/.config/mailblob.json\n", home) + return + } + } + + var mb MailBox + mb.Configure(&configuration.MailBox) err = mb.Connect() if err != nil { log.Fatal(err) } - msg, err := mb.ListMessages() + folders, err := mb.ListFolders() + if err != nil { + log.Fatal(err) + } + log.Println(folders) + msgs, err := mb.GetMessages() if err != nil { log.Fatal(err) } - log.Println(msg) + for _, m := range msgs { + log.Println(m.Id) + log.Println(m.Title) + log.Println(m.Author) + log.Println("Text version") + log.Println(m.Text) + log.Println("HTML version") + log.Println(m.HTML) + log.Println(m.Id) + log.Println() + } mb.Close() - return + http.HandleFunc("/", slash) http.ListenAndServe("0.0.0.0:8080", nil) }