diff --git a/data/control.json b/data/control.json deleted file mode 100644 index ca2facd..0000000 --- a/data/control.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "id": 1, - "world_size": 200, - "open": 1, - "admin_email": "", - "class_1_name": "Mage", - "class_2_name": "Warrior", - "class_3_name": "Paladin" -} \ No newline at end of file diff --git a/internal/control/control.go b/internal/control/control.go index e259f65..7ef4a81 100644 --- a/internal/control/control.go +++ b/internal/control/control.go @@ -15,13 +15,15 @@ var ( // Control represents the game control settings type Control struct { - ID int `json:"id"` - WorldSize int `json:"world_size"` - Open int `json:"open"` - AdminEmail string `json:"admin_email"` - Class1Name string `json:"class_1_name"` - Class2Name string `json:"class_2_name"` - Class3Name string `json:"class_3_name"` + WorldSize int `json:"world_size"` + Open int `json:"open"` + AdminEmail string `json:"admin_email"` + EmailMode string `json:"email_mode"` // "file" or "smtp" + EmailFilePath string `json:"email_file_path"` // path for file mode + SMTPHost string `json:"smtp_host"` + SMTPPort string `json:"smtp_port"` + SMTPUsername string `json:"smtp_username"` + SMTPPassword string `json:"smtp_password"` } // Init loads control settings from the specified JSON file @@ -43,17 +45,13 @@ func Init(jsonFile string) error { if ctrl.WorldSize == 0 { ctrl.WorldSize = defaults.WorldSize } - if ctrl.Class1Name == "" { - ctrl.Class1Name = defaults.Class1Name + if ctrl.EmailMode == "" { + ctrl.EmailMode = defaults.EmailMode } - if ctrl.Class2Name == "" { - ctrl.Class2Name = defaults.Class2Name - } - if ctrl.Class3Name == "" { - ctrl.Class3Name = defaults.Class3Name + if ctrl.EmailFilePath == "" { + ctrl.EmailFilePath = defaults.EmailFilePath } - ctrl.ID = 1 // Ensure singleton ID global = &ctrl } else { // Create default control settings if file doesn't exist @@ -95,13 +93,15 @@ func Save() error { // New creates a new Control with sensible defaults func New() *Control { return &Control{ - ID: 1, // Singleton - WorldSize: 200, - Open: 1, - AdminEmail: "", - Class1Name: "Mage", - Class2Name: "Warrior", - Class3Name: "Paladin", + WorldSize: 200, + Open: 1, + AdminEmail: "", + EmailMode: "file", + EmailFilePath: "emails.txt", + SMTPHost: "", + SMTPPort: "587", + SMTPUsername: "", + SMTPPassword: "", } } @@ -120,7 +120,6 @@ func Set(control *Control) error { mu.Lock() defer mu.Unlock() - control.ID = 1 // Ensure it's always ID 1 (singleton) if err := control.Validate(); err != nil { return err } @@ -171,14 +170,19 @@ func (c *Control) Validate() error { if c.Open != 0 && c.Open != 1 { return fmt.Errorf("Open must be 0 or 1") } - if c.Class1Name == "" { - return fmt.Errorf("Class1Name cannot be empty") + if c.EmailMode != "file" && c.EmailMode != "smtp" { + return fmt.Errorf("EmailMode must be 'file' or 'smtp'") } - if c.Class2Name == "" { - return fmt.Errorf("Class2Name cannot be empty") - } - if c.Class3Name == "" { - return fmt.Errorf("Class3Name cannot be empty") + if c.EmailMode == "smtp" { + if c.SMTPHost == "" { + return fmt.Errorf("SMTPHost required when EmailMode is 'smtp'") + } + if c.SMTPUsername == "" { + return fmt.Errorf("SMTPUsername required when EmailMode is 'smtp'") + } + if c.SMTPPassword == "" { + return fmt.Errorf("SMTPPassword required when EmailMode is 'smtp'") + } } return nil } @@ -199,62 +203,19 @@ func SetOpen(open bool) error { }) } -// GetClassNames returns all class names as a slice -func (c *Control) GetClassNames() []string { - classes := make([]string, 0, 3) - if c.Class1Name != "" { - classes = append(classes, c.Class1Name) - } - if c.Class2Name != "" { - classes = append(classes, c.Class2Name) - } - if c.Class3Name != "" { - classes = append(classes, c.Class3Name) - } - return classes -} - -// GetClassName returns the name of a specific class (1-3) -func (c *Control) GetClassName(classNum int) string { - switch classNum { - case 1: - return c.Class1Name - case 2: - return c.Class2Name - case 3: - return c.Class3Name - default: - return "" - } -} - -// IsValidClassName returns true if the given name matches one of the configured classes -func (c *Control) IsValidClassName(name string) bool { - if name == "" { - return false - } - return name == c.Class1Name || name == c.Class2Name || name == c.Class3Name -} - -// GetClassNumber returns the class number (1-3) for a given class name, or 0 if not found -func (c *Control) GetClassNumber(name string) int { - if name == c.Class1Name && name != "" { - return 1 - } - if name == c.Class2Name && name != "" { - return 2 - } - if name == c.Class3Name && name != "" { - return 3 - } - return 0 -} - // HasAdminEmail returns true if an admin email is configured func (c *Control) HasAdminEmail() bool { return c.AdminEmail != "" } +// IsEmailConfigured returns true if email is properly configured +func (c *Control) IsEmailConfigured() bool { + if c.EmailMode == "file" { + return c.EmailFilePath != "" + } + return c.SMTPHost != "" && c.SMTPUsername != "" && c.SMTPPassword != "" +} + // IsWorldSizeValid returns true if the world size is within reasonable bounds func (c *Control) IsWorldSizeValid() bool { return c.WorldSize > 0 && c.WorldSize <= 10000 diff --git a/internal/helpers/email/emailer.go b/internal/helpers/email/emailer.go new file mode 100644 index 0000000..137ad7a --- /dev/null +++ b/internal/helpers/email/emailer.go @@ -0,0 +1,132 @@ +package email + +import ( + "fmt" + "net/smtp" + "os" + "strings" + "sync" + "time" +) + +var ( + global *Emailer + mu sync.RWMutex +) + +type Emailer struct { + mode string + filePath string + host string + port string + username string + password string +} + +type Email struct { + From string + To []string + Subject string + Body string +} + +// Init initializes the global emailer with the provided settings +func Init(mode, filePath, host, port, username, password string) error { + mu.Lock() + defer mu.Unlock() + + emailer := &Emailer{ + mode: mode, + filePath: filePath, + host: host, + port: port, + username: username, + password: password, + } + + if err := emailer.validate(); err != nil { + return err + } + + global = emailer + return nil +} + +// Get returns the global emailer instance (thread-safe) +func Get() *Emailer { + mu.RLock() + defer mu.RUnlock() + if global == nil { + panic("email not initialized - call Init first") + } + return global +} + +// Send sends an email using the configured method (thread-safe) +func Send(email Email) error { + emailer := Get() + + mu.RLock() + defer mu.RUnlock() + + switch emailer.mode { + case "file": + return emailer.sendToFile(email) + case "smtp": + return emailer.sendViaSMTP(email) + default: + return fmt.Errorf("invalid email mode: %s", emailer.mode) + } +} + +func (e *Emailer) validate() error { + if e.mode != "file" && e.mode != "smtp" { + return fmt.Errorf("mode must be 'file' or 'smtp'") + } + if e.mode == "smtp" { + if e.host == "" || e.username == "" || e.password == "" { + return fmt.Errorf("smtp mode requires host, username, and password") + } + } + if e.mode == "file" && e.filePath == "" { + return fmt.Errorf("file mode requires filePath") + } + return nil +} + +func (e *Emailer) sendToFile(email Email) error { + content := fmt.Sprintf(`--- EMAIL --- +Time: %s +From: %s +To: %s +Subject: %s + +%s + +--- END EMAIL --- + +`, time.Now().Format(time.RFC3339), email.From, strings.Join(email.To, ", "), email.Subject, email.Body) + + f, err := os.OpenFile(e.filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return err + } + defer f.Close() + + _, err = f.WriteString(content) + return err +} + +func (e *Emailer) sendViaSMTP(email Email) error { + auth := smtp.PlainAuth("", e.username, e.password, e.host) + + msg := fmt.Sprintf("From: %s\r\nTo: %s\r\nSubject: %s\r\n\r\n%s", + email.From, + strings.Join(email.To, ", "), + email.Subject, + email.Body, + ) + + addr := e.host + ":" + e.port + return smtp.SendMail(addr, auth, email.From, email.To, []byte(msg)) +} diff --git a/internal/template/template.go b/internal/template/template.go index d1b7839..8aeb56e 100644 --- a/internal/template/template.go +++ b/internal/template/template.go @@ -836,7 +836,7 @@ func (t *Template) getStructField(obj any, fieldName string) any { } rv := reflect.ValueOf(obj) - if rv.Kind() == reflect.Ptr { + if rv.Kind() == reflect.Pointer { if rv.IsNil() { return nil } @@ -976,7 +976,7 @@ func (t *Template) isTruthy(value any) bool { switch rv.Kind() { case reflect.Slice, reflect.Array, reflect.Map: return rv.Len() > 0 - case reflect.Ptr: + case reflect.Pointer: return !rv.IsNil() } return true