Ok pour la lecture des mails, reste à voir comment les servir via http

This commit is contained in:
Laurent Ulrich
2025-07-29 20:40:01 +02:00
parent 881f09fa20
commit e9676f8136
5 changed files with 265 additions and 55 deletions

9
blogentry.go Normal file
View File

@@ -0,0 +1,9 @@
package main
type BlogEntry struct {
Id string
Title string
Author string
HTML string
Text string
}

49
cli.go Normal file
View File

@@ -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
}

View File

@@ -2,17 +2,15 @@ package main
type MailBoxConfiguration struct { type MailBoxConfiguration struct {
Server string Server string
Port int Port string
User string User string
Password string Password string
SSL string SSL string
InBox string
} }
type BlogConfiguration struct { type BlogConfiguration struct {
Title string Title string
MailBox string ShortName string
MailBox MailBoxConfiguration
WWWRoot string WWWRoot string
} }
type Configuration struct {
MailBoxes map[string]MailBoxConfiguration
Blogs map[string]BlogConfiguration
}

View File

@@ -6,6 +6,8 @@ import (
"io" "io"
"log" "log"
"mime" "mime"
"strconv"
"strings"
"github.com/emersion/go-imap/v2" "github.com/emersion/go-imap/v2"
"github.com/emersion/go-imap/v2/imapclient" "github.com/emersion/go-imap/v2/imapclient"
@@ -15,12 +17,28 @@ import (
type MailBox struct { type MailBox struct {
Server string Server string
Port uint16
User string User string
Password string Password string
SSL string
InBox string InBox string
Client *imapclient.Client 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) { func (mb *MailBox) ListMessages() (string, error) {
return "", nil return "", nil
} }
@@ -30,9 +48,19 @@ func (mb *MailBox) Connect() error {
WordDecoder: &mime.WordDecoder{CharsetReader: charset.Reader}, WordDecoder: &mime.WordDecoder{CharsetReader: charset.Reader},
} }
var err error 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 { if err != nil {
log.Println("Error connnecting to", mb.Server, ":", err) log.Println("Error connnecting to", imapServer, ":", err)
return err return err
} }
log.Println("Connected") log.Println("Connected")
@@ -41,21 +69,33 @@ func (mb *MailBox) Connect() error {
log.Fatal("failed to login:", err) log.Fatal("failed to login:", err)
} }
log.Println("Logged in") log.Println("Logged in")
return nil
}
func (mb *MailBox) ListFolders() ([]string, error) {
folders := make([]string, 0)
mailBoxes, err := mb.Client.List("", "%", nil).Collect() mailBoxes, err := mb.Client.List("", "%", nil).Collect()
if err != nil { if err != nil {
log.Fatal("Error listing mailboxes", err) return nil, err
} }
for _, mbox := range mailBoxes { 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() inbox, err := mb.Client.Select(mb.InBox, nil).Wait()
if err != nil { if err != nil {
log.Fatal("Error selecting mailbox:", mb.InBox, err) log.Fatal("Error selecting mailbox:", mb.InBox, err)
} }
log.Println("Inbox has", inbox.NumMessages, "messages") 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) seqSet := imap.SeqSetNum(i)
bodySection := &imap.FetchItemBodySection{} bodySection := &imap.FetchItemBodySection{}
fetchOptions := &imap.FetchOptions{ fetchOptions := &imap.FetchOptions{
@@ -66,44 +106,63 @@ func (mb *MailBox) Connect() error {
if err != nil { if err != nil {
log.Fatal("Error fetching mails:", err) log.Fatal("Error fetching mails:", err)
} }
for _, msg := range messages { entry.Title = strings.Trim(messages[0].Envelope.Subject, "\t ")
log.Println("**************MESSAGE**************") if !strings.HasPrefix(strings.ToLower(entry.Title), "blog ") {
log.Println("From:", msg.Envelope.From) continue
log.Println("To:", msg.Envelope.To) }
log.Println("Date:", msg.Envelope.Date)
log.Println("Subject:", msg.Envelope.Subject)
section := msg.FindBodySection(bodySection) 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) ioReader := bytes.NewReader(section)
mailReader, err := mail.CreateReader(ioReader) mailReader, err := mail.CreateReader(ioReader)
if err != nil { if err != nil {
log.Fatalf("failed to create mail reader: %v", err) return nil, err
} }
for { for {
part, err := mailReader.NextPart() part, err := mailReader.NextPart()
if err == io.EOF { if err == io.EOF {
break break
} else if err != nil { } else if err != nil {
log.Fatal("Error reading part", err) return nil, err
} }
log.Println("------------PART-----------")
fmt.Println("Content-Type:", part.Header.Get("Content-Type")) switch part.Header.(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: case *mail.AttachmentHeader:
/*
filename, _ := header.Filename() filename, _ := header.Filename()
log.Println("------------FILE-----------") log.Println("------------FILE-----------")
log.Println("Attachment:", filename) log.Println("Attachment:", filename)
*/
case *mail.InlineHeader: case *mail.InlineHeader:
log.Println("------------INLINE-----------")
body, _ := io.ReadAll(part.Body) body, _ := io.ReadAll(part.Body)
log.Println(string(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 entries, nil
return nil
} }
func (mb *MailBox) Close() { func (mb *MailBox) Close() {

View File

@@ -1,11 +1,13 @@
package main package main
import ( import (
"encoding/json"
"fmt"
"log" "log"
"net/http" "net/http"
"net/url"
"os" "os"
"strings" "regexp"
"strconv"
) )
func slash(w http.ResponseWriter, r *http.Request) { func slash(w http.ResponseWriter, r *http.Request) {
@@ -14,29 +16,122 @@ func slash(w http.ResponseWriter, r *http.Request) {
func main() { 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 { if err != nil {
log.Fatal("Bad parameter MAILBOX:", os.Getenv("MAILBOX"), "(", err, ")") log.Fatal(err)
} }
mb.Server = url.Host file, err := os.Open(fmt.Sprintf("%s/.config/mailblog.json", home))
mb.User = os.Getenv("IMAP_USER") if err == nil {
mb.Password = os.Getenv("IMAP_PASSWORD") defer file.Close()
mb.InBox, _ = strings.CutPrefix(url.Path, "/") 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() err = mb.Connect()
if err != nil { if err != nil {
log.Fatal(err) 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 { if err != nil {
log.Fatal(err) 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() mb.Close()
return
http.HandleFunc("/", slash) http.HandleFunc("/", slash)
http.ListenAndServe("0.0.0.0:8080", nil) http.ListenAndServe("0.0.0.0:8080", nil)
} }