// Package newsletter manages email subscriber storage as a flat JSON file. package newsletter import ( "encoding/json" "errors" "os" "strings" "sync" "time" ) // Subscriber holds a single email subscription. type Subscriber struct { Email string `json:"email"` CreatedAt time.Time `json:"created_at"` } // Store manages subscribers persisted to a JSON file. type Store struct { mu sync.RWMutex path string } // NewStore returns a Store backed by path, creating the file if needed. func NewStore(path string) (*Store, error) { s := &Store{path: path} // Create empty file if it doesn't exist if _, err := os.Stat(path); os.IsNotExist(err) { if err := s.save(nil); err != nil { return nil, err } } return s, nil } // All returns all current subscribers. func (s *Store) All() ([]Subscriber, error) { s.mu.RLock() defer s.mu.RUnlock() return s.load() } // Add adds an email if it isn't already subscribed. Returns false if duplicate. func (s *Store) Add(email string) (bool, error) { email = strings.ToLower(strings.TrimSpace(email)) if email == "" { return false, errors.New("email is required") } s.mu.Lock() defer s.mu.Unlock() subs, err := s.load() if err != nil { return false, err } for _, sub := range subs { if sub.Email == email { return false, nil // already subscribed } } subs = append(subs, Subscriber{Email: email, CreatedAt: time.Now().UTC()}) return true, s.save(subs) } // Remove unsubscribes an email address. func (s *Store) Remove(email string) error { email = strings.ToLower(strings.TrimSpace(email)) s.mu.Lock() defer s.mu.Unlock() subs, err := s.load() if err != nil { return err } filtered := subs[:0] for _, sub := range subs { if sub.Email != email { filtered = append(filtered, sub) } } return s.save(filtered) } func (s *Store) load() ([]Subscriber, error) { raw, err := os.ReadFile(s.path) if err != nil { return nil, err } if len(raw) == 0 { return nil, nil } var subs []Subscriber if err := json.Unmarshal(raw, &subs); err != nil { return nil, err } return subs, nil } func (s *Store) save(subs []Subscriber) error { if subs == nil { subs = []Subscriber{} } raw, err := json.MarshalIndent(subs, "", " ") if err != nil { return err } return os.WriteFile(s.path, raw, 0600) }