kindle-sender/main.go

187 lines
4.4 KiB
Go

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)
}
}