291 lines
6.3 KiB
Go
291 lines
6.3 KiB
Go
package email
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"reservation-system/backend/internal/db"
|
|
"reservation-system/backend/internal/models"
|
|
|
|
"github.com/emersion/go-imap"
|
|
"github.com/emersion/go-imap/client"
|
|
)
|
|
|
|
type EmailProcessor struct {
|
|
db *db.DB
|
|
running bool
|
|
stop chan bool
|
|
}
|
|
|
|
func NewEmailProcessor(database *db.DB) *EmailProcessor {
|
|
return &EmailProcessor{
|
|
db: database,
|
|
stop: make(chan bool),
|
|
}
|
|
}
|
|
|
|
func (ep *EmailProcessor) Start() {
|
|
if ep.running {
|
|
return
|
|
}
|
|
ep.running = true
|
|
go ep.pollLoop()
|
|
}
|
|
|
|
func (ep *EmailProcessor) Stop() {
|
|
ep.running = false
|
|
close(ep.stop)
|
|
}
|
|
|
|
func (ep *EmailProcessor) pollLoop() {
|
|
ticker := time.NewTicker(5 * time.Minute)
|
|
defer ticker.Stop()
|
|
|
|
// Run immediately on start
|
|
ep.processEmails()
|
|
|
|
for {
|
|
select {
|
|
case <-ticker.C:
|
|
if !ep.running {
|
|
return
|
|
}
|
|
ep.processEmails()
|
|
case <-ep.stop:
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (ep *EmailProcessor) processEmails() error {
|
|
cfg, err := ep.db.GetEmailConfig()
|
|
if err != nil {
|
|
log.Printf("Email config error: %v", err)
|
|
return err
|
|
}
|
|
if cfg == nil {
|
|
log.Println("No email config found")
|
|
return nil
|
|
}
|
|
|
|
// Connect to IMAP server
|
|
addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)
|
|
var c *client.Client
|
|
|
|
if cfg.SSL {
|
|
c, err = client.DialTLS(addr, &tls.Config{InsecureSkipVerify: true})
|
|
} else {
|
|
c, err = client.Dial(addr)
|
|
}
|
|
if err != nil {
|
|
log.Printf("IMAP connect error: %v", err)
|
|
return err
|
|
}
|
|
defer c.Logout()
|
|
|
|
// Login
|
|
if err := c.Login(cfg.Username, cfg.Password); err != nil {
|
|
log.Printf("IMAP login error: %v", err)
|
|
return err
|
|
}
|
|
|
|
// Select inbox
|
|
mbox, err := c.Select(cfg.Folder, false)
|
|
if err != nil {
|
|
log.Printf("IMAP select error: %v", err)
|
|
return err
|
|
}
|
|
|
|
if mbox.Messages == 0 {
|
|
return nil
|
|
}
|
|
|
|
// Fetch unread messages
|
|
criteria := imap.NewSearchCriteria()
|
|
criteria.WithoutFlags = []string{imap.SeenFlag}
|
|
uids, err := c.UidSearch(criteria)
|
|
if err != nil {
|
|
log.Printf("IMAP search error: %v", err)
|
|
return err
|
|
}
|
|
|
|
if len(uids) == 0 {
|
|
return nil
|
|
}
|
|
|
|
for _, uid := range uids {
|
|
ep.processMessage(c, uid)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (ep *EmailProcessor) processMessage(c *client.Client, uid uint32) {
|
|
seqSet := new(imap.SeqSet)
|
|
seqSet.AddNum(uid)
|
|
|
|
section := &imap.BodySectionName{}
|
|
items := []imap.FetchItem{section.FetchItem()}
|
|
|
|
messages := make(chan *imap.Message, 1)
|
|
go func() {
|
|
if err := c.UidFetch(seqSet, items, messages); err != nil {
|
|
log.Printf("Fetch error: %v", err)
|
|
}
|
|
}()
|
|
|
|
msg := <-messages
|
|
if msg == nil {
|
|
return
|
|
}
|
|
|
|
r := msg.GetBody(section)
|
|
if r == nil {
|
|
return
|
|
}
|
|
|
|
body, err := io.ReadAll(r)
|
|
if err != nil {
|
|
log.Printf("Read error: %v", err)
|
|
return
|
|
}
|
|
|
|
// Parse reservation from email
|
|
reservation := ep.parseReservation(string(body))
|
|
if reservation != nil {
|
|
// Find available table
|
|
tables, _ := ep.db.GetAllTables()
|
|
for _, table := range tables {
|
|
available, _ := ep.db.CheckAvailability(table.ID, reservation.Date, reservation.TimeFrom, reservation.TimeTo)
|
|
if available && table.MaxGuests >= reservation.Guests {
|
|
reservation.TableID = table.ID
|
|
reservation.Source = "email"
|
|
if err := ep.db.CreateReservation(reservation); err != nil {
|
|
log.Printf("Create reservation error: %v", err)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// Mark as seen
|
|
seqSet2 := new(imap.SeqSet)
|
|
seqSet2.AddNum(uid)
|
|
c.UidStore(seqSet2, imap.FormatFlagsAdd, []string{imap.SeenFlag})
|
|
}
|
|
|
|
func (ep *EmailProcessor) parseReservation(body string) *models.Reservation {
|
|
res := &models.Reservation{}
|
|
parsed := false
|
|
|
|
// Try to extract date
|
|
datePatterns := []*regexp.Regexp{
|
|
regexp.MustCompile(`(?i)(?:am|den)\s+(\d{1,2})[./](\d{1,2})[./](\d{2,4})`),
|
|
regexp.MustCompile(`(?i)(\d{1,2})[./](\d{1,2})[./](\d{2,4})`),
|
|
regexp.MustCompile(`(?i)(\d{4})-(\d{2})-(\d{2})`),
|
|
}
|
|
|
|
for _, pattern := range datePatterns {
|
|
if matches := pattern.FindStringSubmatch(body); matches != nil {
|
|
if len(matches) >= 4 {
|
|
if len(matches[3]) == 2 {
|
|
res.Date = fmt.Sprintf("20%s-%s-%s", matches[3], matches[2], matches[1])
|
|
} else {
|
|
res.Date = fmt.Sprintf("%s-%s-%s", matches[3], matches[2], matches[1])
|
|
}
|
|
parsed = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// Try to extract time
|
|
timePatterns := []*regexp.Regexp{
|
|
regexp.MustCompile(`(?i)(\d{1,2})[:.](\d{2})\s*[Uu]hr`),
|
|
regexp.MustCompile(`(?i)(\d{1,2})[:.](\d{2})`),
|
|
regexp.MustCompile(`(?i)(\d{1,2})\s*[Uu]hr`),
|
|
}
|
|
|
|
for _, pattern := range timePatterns {
|
|
if matches := pattern.FindStringSubmatch(body); matches != nil {
|
|
if len(matches) >= 2 {
|
|
res.TimeFrom = fmt.Sprintf("%02s:00", matches[1])
|
|
res.TimeTo = fmt.Sprintf("%02d:00", parseInt(matches[1])+2)
|
|
parsed = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// Try to extract guest count
|
|
guestPatterns := []*regexp.Regexp{
|
|
regexp.MustCompile(`(?i)(\d+)\s*(?:Personen|Gäste|Leute)`),
|
|
regexp.MustCompile(`(?i)für\s+(\d+)\s*Personen`),
|
|
regexp.MustCompile(`(?i)(\d+)\s*Personen`),
|
|
}
|
|
|
|
for _, pattern := range guestPatterns {
|
|
if matches := pattern.FindStringSubmatch(body); matches != nil {
|
|
res.Guests = parseInt(matches[1])
|
|
parsed = true
|
|
break
|
|
}
|
|
}
|
|
|
|
// Try to extract name
|
|
namePatterns := []*regexp.Regexp{
|
|
regexp.MustCompile(`(?i)Name\s*[:\-]?\s*([^\n\r]+)`),
|
|
regexp.MustCompile(`(?i)(?:von|Name)\s+([A-Z][a-z]+\s+[A-Z][a-z]+)`),
|
|
}
|
|
|
|
for _, pattern := range namePatterns {
|
|
if matches := pattern.FindStringSubmatch(body); matches != nil {
|
|
res.Name = strings.TrimSpace(matches[1])
|
|
parsed = true
|
|
break
|
|
}
|
|
}
|
|
|
|
// Try to extract phone
|
|
phonePattern := regexp.MustCompile(`(?i)Tel(?:efon)?[:\s]*([\d\s\-\+\(\)]{7,})`)
|
|
if matches := phonePattern.FindStringSubmatch(body); matches != nil {
|
|
res.Phone = strings.TrimSpace(matches[1])
|
|
}
|
|
|
|
// Try to extract email
|
|
emailPattern := regexp.MustCompile(`[\w\.-]+@[\w\.-]+\.\w+`)
|
|
if matches := emailPattern.FindStringSubmatch(body); matches != nil {
|
|
res.Email = matches[0]
|
|
}
|
|
|
|
// Try to extract notes
|
|
notePatterns := []*regexp.Regexp{
|
|
regexp.MustCompile(`(?i)Notiz(?:en)?\s*[:\-]?\s*([^\n\r]+)`),
|
|
regexp.MustCompile(`(?i)Bemerkung(?:en)?\s*[:\-]?\s*([^\n\r]+)`),
|
|
}
|
|
|
|
for _, pattern := range notePatterns {
|
|
if matches := pattern.FindStringSubmatch(body); matches != nil {
|
|
res.Notes = strings.TrimSpace(matches[1])
|
|
break
|
|
}
|
|
}
|
|
|
|
if parsed && res.Name != "" {
|
|
return res
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func parseInt(s string) int {
|
|
var n int
|
|
fmt.Sscanf(s, "%d", &n)
|
|
return n
|
|
}
|