package main import ( "bytes" "crypto/md5" "encoding/base64" "encoding/hex" "encoding/json" "fmt" "io" "log" "mime/multipart" "mime/quotedprintable" "net/smtp" "os" "path/filepath" "strings" "sync" "time" ) 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") sentDir string sentFileLock sync.Mutex ) type SentFileEntry struct { Path string `json:"path"` SentAt time.Time `json:"sent_at"` Hash string `json:"hash"` } 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 sentFilePath(hash string) string { return filepath.Join(sentDir, hash+".json") } func hasBeenSent(hash string) bool { sentFileLock.Lock() defer sentFileLock.Unlock() _, err := os.Stat(sentFilePath(hash)) return err == nil } func markAsSent(filePath, hash string) { sentFileLock.Lock() defer sentFileLock.Unlock() entry := SentFileEntry{ Path: filePath, SentAt: time.Now().UTC(), Hash: hash, } f, err := os.Create(sentFilePath(hash)) if err != nil { log.Printf("Failed to record sent file %s: %v", filePath, err) return } defer f.Close() json.NewEncoder(f).Encode(entry) } func setupFileTracking() error { sentDir = filepath.Join(watchDir, ".sent") return os.MkdirAll(sentDir, 0755) } 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 := setupFileTracking(); err != nil { log.Fatalf("Failed to setup tracking directory: %v", err) } log.Printf("Scanning folder: %s\n", watchDir) for { scanAndSendEbooks() time.Sleep(10 * time.Second) } }