commit fd20f431b4f8af73dc22493fd145b93bec96f08d Author: brandon Date: Fri Jun 6 19:56:06 2025 -0700 initial commit diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..d145b05 Binary files /dev/null and b/.DS_Store differ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..106c786 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/mailboxes/ +users.json \ No newline at end of file diff --git a/bang.go b/bang.go new file mode 100644 index 0000000..1813275 --- /dev/null +++ b/bang.go @@ -0,0 +1,246 @@ +package main + +import ( + "bytes" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "fmt" + "io/ioutil" + "io" + "log" + "net/http" + "os" + "path/filepath" + "sync" + "time" +) + +type Email struct { + ID string `json:"id"` + From string `json:"from"` + To string `json:"to"` + Subject string `json:"subject"` + Body string `json:"body"` + Timestamp time.Time `json:"timestamp"` + Domain string `json:"domain"` +} + +// User struct for account management +type User struct { + Username string `json:"username"` + Password string `json:"password"` // hashed +} + +var ( + mailboxDir = "mailboxes" + mu sync.Mutex + usersFile = "users.json" +) + +// Load users from file +func loadUsers() (map[string]string, error) { + users := make(map[string]string) + data, err := ioutil.ReadFile(usersFile) + if err != nil { + if os.IsNotExist(err) { + return users, nil + } + return nil, err + } + var userList []User + if err := json.Unmarshal(data, &userList); err != nil { + return nil, err + } + for _, u := range userList { + users[u.Username] = u.Password + } + return users, nil +} + +// Save users to file +func saveUsers(users map[string]string) error { + var userList []User + for username, password := range users { + userList = append(userList, User{Username: username, Password: password}) + } + data, err := json.MarshalIndent(userList, "", " ") + if err != nil { + return err + } + return ioutil.WriteFile(usersFile, data, 0644) +} + +// Hash password +func hashPassword(password string) string { + h := sha256.Sum256([]byte(password)) + return base64.StdEncoding.EncodeToString(h[:]) +} + +func saveEmail(email Email) error { + mu.Lock() + defer mu.Unlock() + userDir := filepath.Join(mailboxDir, email.To) + if err := os.MkdirAll(userDir, 0755); err != nil { + return err + } + filePath := filepath.Join(userDir, email.ID+".json") + f, err := os.Create(filePath) + if err != nil { + return err + } + defer f.Close() + return json.NewEncoder(f).Encode(email) +} + +func createUserHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + var req struct { + Username string `json:"username"` + Password string `json:"password"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid JSON", http.StatusBadRequest) + return + } + if req.Username == "" || req.Password == "" { + http.Error(w, "Username and password required", http.StatusBadRequest) + return + } + mu.Lock() + defer mu.Unlock() + users, err := loadUsers() + if err != nil { + http.Error(w, "Server error", http.StatusInternalServerError) + return + } + if _, exists := users[req.Username]; exists { + http.Error(w, "User already exists", http.StatusConflict) + return + } + hashed := hashPassword(req.Password) + users[req.Username] = hashed + if err := saveUsers(users); err != nil { + http.Error(w, "Server error", http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(map[string]string{"status": "user created"}) +} + +// Authenticate user from Basic Auth +func authenticate(r *http.Request) (string, bool) { + username, password, ok := r.BasicAuth() + if !ok { + return "", false + } + users, err := loadUsers() + if err != nil { + return "", false + } + hashed := hashPassword(password) + if users[username] == hashed { + return username, true + } + return "", false +} + +func receiveEmailHandler(w http.ResponseWriter, r *http.Request) { + _, ok := authenticate(r) + if !ok { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + var email Email + if err := json.NewDecoder(r.Body).Decode(&email); err != nil { + http.Error(w, "Invalid JSON", http.StatusBadRequest) + return + } + if email.Domain == "brandon.ad" { + email.ID = fmt.Sprintf("%d", time.Now().UnixNano()) + email.Timestamp = time.Now() + if err := saveEmail(email); err != nil { + http.Error(w, "Failed to save email", http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(map[string]string{"status": "ok", "id": email.ID}) + return + } + // Forward to another domain + forwardURL := fmt.Sprintf("http://%s/email", email.Domain) + jsonData, err := json.Marshal(email) + if err != nil { + http.Error(w, "Failed to marshal email", http.StatusInternalServerError) + return + } + resp, err := http.Post(forwardURL, "application/json", bytes.NewReader(jsonData)) + if err != nil { + http.Error(w, "Failed to forward email", http.StatusBadGateway) + return + } + defer resp.Body.Close() + w.WriteHeader(resp.StatusCode) + io.Copy(w, resp.Body) +} + +func listMailboxHandler(w http.ResponseWriter, r *http.Request) { + username, ok := authenticate(r) + if !ok { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + user := r.URL.Query().Get("user") + if user == "" { + http.Error(w, "Missing user", http.StatusBadRequest) + return + } + if user != username { + http.Error(w, "Forbidden", http.StatusForbidden) + return + } + userDir := filepath.Join(mailboxDir, user) + files, err := os.ReadDir(userDir) + if err != nil { + http.Error(w, "Mailbox not found", http.StatusNotFound) + return + } + var emails []Email + for _, file := range files { + f, err := os.Open(filepath.Join(userDir, file.Name())) + if err != nil { + continue + } + var email Email + if err := json.NewDecoder(f).Decode(&email); err == nil { + emails = append(emails, email) + } + f.Close() + } + json.NewEncoder(w).Encode(emails) +} + +// CORS middleware +func withCORS(handler http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") + if r.Method == http.MethodOptions { + w.WriteHeader(http.StatusOK) + return + } + handler(w, r) + } +} + +func main() { + http.HandleFunc("/users", withCORS(createUserHandler)) + http.HandleFunc("/email", withCORS(receiveEmailHandler)) + http.HandleFunc("/mailbox", withCORS(listMailboxHandler)) + log.Println("Email server running on :8080") + log.Fatal(http.ListenAndServe(":8080", nil)) +} diff --git a/testclient.py b/testclient.py new file mode 100644 index 0000000..2a5f6ae --- /dev/null +++ b/testclient.py @@ -0,0 +1,58 @@ +import requests +import json + +def create_account(username, password): + url = "http://localhost:8080/users" + data = {"username": username, "password": password} + headers = {"Content-Type": "application/json"} + response = requests.post(url, data=json.dumps(data), headers=headers) + print("Create Account Status:", response.status_code) + try: + print("Create Account Response:", response.json()) + except Exception: + print("Create Account Response Text:", response.text) + return response + +def check_mailbox(username, password): + url = f"http://localhost:8080/mailbox?user={username}" + response = requests.get(url, auth=(username, password)) + print("\nMailbox Status:", response.status_code) + try: + emails = response.json() + print(f"Mailbox for {username}:") + for email in emails: + print(json.dumps(email, indent=2)) + except Exception: + print("Mailbox Response Text:", response.text) + return response + +url = "http://localhost:8080/email" + +email = { + "from": "alice", + "to": "eric", + "subject": "Hello from Python client", + "body": "This is a test email sent from the Python client." +} + +headers = {"Content-Type": "application/json"} + +# Set your test username and password here +username = "eric" +password = "1234" # Replace with the actual password for 'bob' + +# Uncomment to create the account before sending email +# print(create_account(username, password)) + +response = requests.post(url, data=json.dumps(email), headers=headers, auth=(username, password)) + +print(check_mailbox(username, password)) + +print("Status Code:", response.status_code) +try: + print("Response:", response.json()) +except Exception: + print("Response Text:", response.text) + +# Uncomment to check mailbox after sending email +# check_mailbox(username, password)