v2.0: 3-Raum-System - Hauptraum, Saal A, Saal B mit 18 Tischen, Raum-Buchungen, API-Doku
This commit is contained in:
@@ -0,0 +1,290 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user