1package config
2
3import (
4 "encoding/json"
5 "os"
6 "path/filepath"
7
8 "github.com/google/uuid"
9)
10
11// Account stores the configuration for a single email account.
12type Account struct {
13 ID string `json:"id"`
14 Name string `json:"name"`
15 Email string `json:"email"`
16 Password string `json:"password"`
17 ServiceProvider string `json:"service_provider"` // "gmail", "icloud", or "custom"
18 // FetchEmail is the single email address for which messages should be fetched.
19 // If empty, it will default to `Email` when accounts are added.
20 FetchEmail string `json:"fetch_email,omitempty"`
21
22 // Custom server settings (used when ServiceProvider is "custom")
23 IMAPServer string `json:"imap_server,omitempty"`
24 IMAPPort int `json:"imap_port,omitempty"`
25 SMTPServer string `json:"smtp_server,omitempty"`
26 SMTPPort int `json:"smtp_port,omitempty"`
27}
28
29// Config stores the user's email configuration with multiple accounts.
30type Config struct {
31 Accounts []Account `json:"accounts"`
32 DisableImages bool `json:"disable_images,omitempty"`
33}
34
35// GetIMAPServer returns the IMAP server address for the account.
36func (a *Account) GetIMAPServer() string {
37 switch a.ServiceProvider {
38 case "gmail":
39 return "imap.gmail.com"
40 case "icloud":
41 return "imap.mail.me.com"
42 case "custom":
43 return a.IMAPServer
44 default:
45 return ""
46 }
47}
48
49// GetIMAPPort returns the IMAP port for the account.
50func (a *Account) GetIMAPPort() int {
51 switch a.ServiceProvider {
52 case "gmail", "icloud":
53 return 993
54 case "custom":
55 if a.IMAPPort != 0 {
56 return a.IMAPPort
57 }
58 return 993 // Default IMAP SSL port
59 default:
60 return 993
61 }
62}
63
64// GetSMTPServer returns the SMTP server address for the account.
65func (a *Account) GetSMTPServer() string {
66 switch a.ServiceProvider {
67 case "gmail":
68 return "smtp.gmail.com"
69 case "icloud":
70 return "smtp.mail.me.com"
71 case "custom":
72 return a.SMTPServer
73 default:
74 return ""
75 }
76}
77
78// GetSMTPPort returns the SMTP port for the account.
79func (a *Account) GetSMTPPort() int {
80 switch a.ServiceProvider {
81 case "gmail", "icloud":
82 return 587
83 case "custom":
84 if a.SMTPPort != 0 {
85 return a.SMTPPort
86 }
87 return 587 // Default SMTP TLS port
88 default:
89 return 587
90 }
91}
92
93// configDir returns the path to the configuration directory.
94func configDir() (string, error) {
95 home, err := os.UserHomeDir()
96 if err != nil {
97 return "", err
98 }
99 return filepath.Join(home, ".config", "matcha"), nil
100}
101
102// configFile returns the full path to the configuration file.
103func configFile() (string, error) {
104 dir, err := configDir()
105 if err != nil {
106 return "", err
107 }
108 return filepath.Join(dir, "config.json"), nil
109}
110
111// SaveConfig saves the given configuration to the config file.
112func SaveConfig(config *Config) error {
113 path, err := configFile()
114 if err != nil {
115 return err
116 }
117 if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
118 return err
119 }
120 data, err := json.MarshalIndent(config, "", " ")
121 if err != nil {
122 return err
123 }
124 return os.WriteFile(path, data, 0600)
125}
126
127// LoadConfig loads the configuration from the config file.
128func LoadConfig() (*Config, error) {
129 path, err := configFile()
130 if err != nil {
131 return nil, err
132 }
133 data, err := os.ReadFile(path)
134 if err != nil {
135 return nil, err
136 }
137 var config Config
138 if err := json.Unmarshal(data, &config); err != nil {
139 // Try to load legacy single-account config
140 var legacyConfig legacyConfigFormat
141 if legacyErr := json.Unmarshal(data, &legacyConfig); legacyErr == nil && legacyConfig.Email != "" {
142 // Convert legacy config to new format
143 config = Config{
144 Accounts: []Account{
145 {
146 ID: uuid.New().String(),
147 Name: legacyConfig.Name,
148 Email: legacyConfig.Email,
149 Password: legacyConfig.Password,
150 ServiceProvider: legacyConfig.ServiceProvider,
151 // Default FetchEmail to the legacy Email value
152 FetchEmail: legacyConfig.Email,
153 },
154 },
155 }
156 // Save the migrated config
157 if saveErr := SaveConfig(&config); saveErr != nil {
158 return nil, saveErr
159 }
160 return &config, nil
161 }
162 return nil, err
163 }
164 return &config, nil
165}
166
167// legacyConfigFormat represents the old single-account configuration format.
168type legacyConfigFormat struct {
169 ServiceProvider string `json:"service_provider"`
170 Email string `json:"email"`
171 Password string `json:"password"`
172 Name string `json:"name"`
173}
174
175// AddAccount adds a new account to the configuration.
176func (c *Config) AddAccount(account Account) {
177 if account.ID == "" {
178 account.ID = uuid.New().String()
179 }
180 // Ensure FetchEmail defaults to the login Email if not explicitly set.
181 if account.FetchEmail == "" && account.Email != "" {
182 account.FetchEmail = account.Email
183 }
184 c.Accounts = append(c.Accounts, account)
185}
186
187// RemoveAccount removes an account by its ID.
188func (c *Config) RemoveAccount(id string) bool {
189 for i, acc := range c.Accounts {
190 if acc.ID == id {
191 c.Accounts = append(c.Accounts[:i], c.Accounts[i+1:]...)
192 return true
193 }
194 }
195 return false
196}
197
198// GetAccountByID returns an account by its ID.
199func (c *Config) GetAccountByID(id string) *Account {
200 for i := range c.Accounts {
201 if c.Accounts[i].ID == id {
202 return &c.Accounts[i]
203 }
204 }
205 return nil
206}
207
208// GetAccountByEmail returns an account by its email address.
209func (c *Config) GetAccountByEmail(email string) *Account {
210 for i := range c.Accounts {
211 if c.Accounts[i].Email == email {
212 return &c.Accounts[i]
213 }
214 }
215 return nil
216}
217
218// HasAccounts returns true if there are any configured accounts.
219func (c *Config) HasAccounts() bool {
220 return len(c.Accounts) > 0
221}
222
223// GetFirstAccount returns the first account or nil if none exist.
224func (c *Config) GetFirstAccount() *Account {
225 if len(c.Accounts) > 0 {
226 return &c.Accounts[0]
227 }
228 return nil
229}