Files

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
}