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 }