package main import ( "bytes" "crypto/md5" "encoding/base64" "encoding/hex" "fmt" "io" "log" "mime/multipart" "mime/quotedprintable" "net/smtp" "os" "path/filepath" "strings" "time" "gorm.io/driver/sqlite" "gorm.io/gorm" ) var ( smtpHost = os.Getenv("SMTP_HOST") smtpPort = os.Getenv("SMTP_PORT") smtpUser = os.Getenv("SMTP_USER") smtpPassword = os.Getenv("SMTP_PASSWORD") senderEmail = os.Getenv("SENDER_EMAIL") recipient = os.Getenv("RECIPIENT_EMAIL") watchDir = os.Getenv("WATCH_DIR") db *gorm.DB ) type SentFile struct { ID uint `gorm:"primaryKey"` Path string Hash string `gorm:"uniqueIndex"` SentAt time.Time } func isEbookFile(name string) bool { lower := strings.ToLower(name) return strings.HasSuffix(lower, ".mobi") || strings.HasSuffix(lower, ".epub") || strings.HasSuffix(lower, ".pdf") || strings.HasSuffix(lower, ".azw3") } func sendEmailWithAttachment(filePath string) error { data, err := os.ReadFile(filePath) if err != nil { return fmt.Errorf("failed to read file: %w", err) } var buf bytes.Buffer writer := multipart.NewWriter(&buf) header := map[string]string{ "From": senderEmail, "To": recipient, "Subject": "Ebook Delivery", "MIME-Version": "1.0", "Content-Type": "multipart/mixed; boundary=\"" + writer.Boundary() + "\"", } for k, v := range header { fmt.Fprintf(&buf, "%s: %s\r\n", k, v) } fmt.Fprint(&buf, "\r\n") textPart, _ := writer.CreatePart(map[string][]string{ "Content-Type": {"text/plain; charset=utf-8"}, "Content-Transfer-Encoding": {"quoted-printable"}, }) qp := quotedprintable.NewWriter(textPart) qp.Write([]byte("Attached is your ebook.")) qp.Close() attachmentHeader := map[string][]string{ "Content-Type": {"application/octet-stream"}, "Content-Transfer-Encoding": {"base64"}, "Content-Disposition": {fmt.Sprintf("attachment; filename=\"%s\"", filepath.Base(filePath))}, } attachmentPart, _ := writer.CreatePart(attachmentHeader) encoded := make([]byte, base64.StdEncoding.EncodedLen(len(data))) base64.StdEncoding.Encode(encoded, data) attachmentPart.Write(encoded) writer.Close() auth := smtp.PlainAuth("", smtpUser, smtpPassword, smtpHost) return smtp.SendMail(smtpHost+":"+smtpPort, auth, senderEmail, []string{recipient}, buf.Bytes()) } func fileHash(path string) (string, error) { f, err := os.Open(path) if err != nil { return "", err } defer f.Close() h := md5.New() if _, err := io.Copy(h, f); err != nil { return "", err } return hex.EncodeToString(h.Sum(nil)), nil } func hasBeenSent(hash string) bool { var file SentFile err := db.Where("hash = ?", hash).First(&file).Error return err == nil } func markAsSent(filePath, hash string) { err := db.Create(&SentFile{ Path: filePath, Hash: hash, SentAt: time.Now().UTC(), }).Error if err != nil { log.Printf("DB error marking file as sent: %v", err) } } func setupDatabase() error { dbPath := filepath.Join(watchDir, "sent_files.db") database, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{}) if err != nil { return err } db = database return db.AutoMigrate(&SentFile{}) } func scanAndSendEbooks() { err := filepath.Walk(watchDir, func(path string, info os.FileInfo, err error) error { if err != nil { return err } if info.IsDir() || !isEbookFile(info.Name()) { return nil } hash, err := fileHash(path) if err != nil { log.Printf("Error hashing file %s: %v", path, err) return nil } if hasBeenSent(hash) { log.Printf("Already sent (hash match): %s\n", path) return nil } log.Printf("Detected ebook file: %s\n", path) time.Sleep(1 * time.Second) if err := sendEmailWithAttachment(path); err != nil { log.Printf("Error sending email: %v\n", err) } else { markAsSent(path, hash) log.Printf("Sent %s to %s\n", path, recipient) } return nil }) if err != nil { log.Printf("Error walking directory: %v\n", err) } } func main() { if watchDir == "" { log.Fatal("WATCH_DIR environment variable is required") } if err := setupDatabase(); err != nil { log.Fatalf("Failed to setup database: %v", err) } log.Printf("Scanning folder: %s\n", watchDir) for { scanAndSendEbooks() time.Sleep(10 * time.Second) } }