Skip to main content

max / goingson

Take email-account create/update fields by struct, not position create() was eleven positional arguments with two adjacent server pairs and an adjacent username/password pair; update() was twelve. Every one of those swaps compiled. NewEmailAccount/UpdateEmailAccount name them.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-25 17:16 UTC
Signed with PGP, not checked
Commit: 12b5c47d9cc9b038895bb5c6499b85c381d440cf
Parent: c3545b2
8 files changed, +269 insertions, -216 deletions
@@ -80,14 +80,14 @@
80 80 Annotation, Attachment, AttachmentMeta, BackupSettings, BlockType, CssClass, DailyNote,
81 81 DbValue, Email, EmailAccount, EmailAuthType, EmailThread, Event, FolderSyncState, Milestone,
82 82 MilestoneStatus, MonthlyGoal, MonthlyGoalStatus, MonthlyReflection, MonthlySpec, NewAttachment,
83 - NewBackupSettings, NewEmail, NewEmailWithTracking, NewEvent, NewEventBuilder, NewMilestone,
84 - NewProblem, NewProject, NewSavedView, NewTask, NewTaskBuilder, ParseableEnum, PositiveMinutes,
85 - Priority, Problem, ProblemBand, ProblemStatus, Project, ProjectStatus, ProjectType, Recurrence,
86 - RecurrenceRule, SavedView, SortDirection, SortField, StatusToken, Subtask, SyncAccount,
87 - TOKEN_KIND_COMMIT, Task, TaskFilterQuery, TaskSortColumn, TaskStatus, TimeSession,
88 - TimeSummaryPanel, TimeSummaryProject, TimeTrackingSummary, TokenState, UpdateEvent,
89 - UpdateProject, UpdateTask, User, ViewFilters, ViewType, WeeklyReview, format_file_size,
90 - mime_from_extension, roll_up_time_summary, snap_all_day_span,
83 + NewBackupSettings, NewEmail, NewEmailAccount, NewEmailWithTracking, NewEvent, NewEventBuilder,
84 + NewMilestone, NewProblem, NewProject, NewSavedView, NewTask, NewTaskBuilder, ParseableEnum,
85 + PositiveMinutes, Priority, Problem, ProblemBand, ProblemStatus, Project, ProjectStatus,
86 + ProjectType, Recurrence, RecurrenceRule, SavedView, SortDirection, SortField, StatusToken,
87 + Subtask, SyncAccount, TOKEN_KIND_COMMIT, Task, TaskFilterQuery, TaskSortColumn, TaskStatus,
88 + TimeSession, TimeSummaryPanel, TimeSummaryProject, TimeTrackingSummary, TokenState,
89 + UpdateEmailAccount, UpdateEvent, UpdateProject, UpdateTask, User, ViewFilters, ViewType,
90 + WeeklyReview, format_file_size, mime_from_extension, roll_up_time_summary, snap_all_day_span,
91 91 };
92 92 pub use parser::{ParseResult, ParsedTask, parse_quick_add, parse_quick_add_with_warnings};
93 93 pub use recurrence::{
@@ -3,7 +3,7 @@
3 3 mod common;
4 4
5 5 use chrono::{Duration, Utc};
6 - use goingson_core::{EmailAccountRepository, EmailAuthType};
6 + use goingson_core::{EmailAccountRepository, EmailAuthType, NewEmailAccount, UpdateEmailAccount};
7 7 use goingson_db_sqlite::SqliteEmailAccountRepository;
8 8
9 9 #[tokio::test]
@@ -15,16 +15,18 @@
15 15 let created = repo
16 16 .create(
17 17 user_id,
18 - "Work Email",
19 - "work@example.com",
20 - "imap.example.com",
21 - 993,
22 - "smtp.example.com",
23 - 587,
24 - "work@example.com",
25 - "secret123",
26 - true,
27 - Some("Archive"),
18 + NewEmailAccount {
19 + account_name: "Work Email",
20 + email_address: "work@example.com",
21 + imap_server: "imap.example.com",
22 + imap_port: 993,
23 + smtp_server: "smtp.example.com",
24 + smtp_port: 587,
25 + username: "work@example.com",
26 + password: "secret123",
27 + use_tls: true,
28 + archive_folder_name: Some("Archive"),
29 + },
28 30 )
29 31 .await
30 32 .expect("Failed to create account");
@@ -142,32 +144,36 @@
142 144
143 145 repo.create(
144 146 user_id,
145 - "Beta Account",
146 - "beta@example.com",
147 - "imap.beta.com",
148 - 993,
149 - "smtp.beta.com",
150 - 587,
151 - "beta",
152 - "pass",
153 - true,
154 - None,
147 + NewEmailAccount {
148 + account_name: "Beta Account",
149 + email_address: "beta@example.com",
150 + imap_server: "imap.beta.com",
151 + imap_port: 993,
152 + smtp_server: "smtp.beta.com",
153 + smtp_port: 587,
154 + username: "beta",
155 + password: "pass",
156 + use_tls: true,
157 + archive_folder_name: None,
158 + },
155 159 )
156 160 .await
157 161 .expect("Failed to create beta account");
158 162
159 163 repo.create(
160 164 user_id,
161 - "Alpha Account",
162 - "alpha@example.com",
163 - "imap.alpha.com",
164 - 993,
165 - "smtp.alpha.com",
166 - 587,
167 - "alpha",
168 - "pass",
169 - true,
170 - None,
165 + NewEmailAccount {
166 + account_name: "Alpha Account",
167 + email_address: "alpha@example.com",
168 + imap_server: "imap.alpha.com",
169 + imap_port: 993,
170 + smtp_server: "smtp.alpha.com",
171 + smtp_port: 587,
172 + username: "alpha",
173 + password: "pass",
174 + use_tls: true,
175 + archive_folder_name: None,
176 + },
171 177 )
172 178 .await
173 179 .expect("Failed to create alpha account");
@@ -190,16 +196,18 @@
190 196 let created = repo
191 197 .create(
192 198 user_id,
193 - "Original",
194 - "old@example.com",
195 - "imap.old.com",
196 - 993,
197 - "smtp.old.com",
198 - 587,
199 - "olduser",
200 - "oldpass",
201 - true,
202 - None,
199 + NewEmailAccount {
200 + account_name: "Original",
201 + email_address: "old@example.com",
202 + imap_server: "imap.old.com",
203 + imap_port: 993,
204 + smtp_server: "smtp.old.com",
205 + smtp_port: 587,
206 + username: "olduser",
207 + password: "oldpass",
208 + use_tls: true,
209 + archive_folder_name: None,
210 + },
203 211 )
204 212 .await
205 213 .expect("Failed to create account");
@@ -208,16 +216,18 @@
208 216 .update(
209 217 created.id,
210 218 user_id,
211 - "Updated Name",
212 - "new@example.com",
213 - "imap.new.com",
214 - 995,
215 - "smtp.new.com",
216 - 465,
217 - "newuser",
218 - Some("newpass"),
219 - false,
220 - Some("All Mail"),
219 + UpdateEmailAccount {
220 + account_name: "Updated Name",
221 + email_address: "new@example.com",
222 + imap_server: "imap.new.com",
223 + imap_port: 995,
224 + smtp_server: "smtp.new.com",
225 + smtp_port: 465,
226 + username: "newuser",
227 + password: Some("newpass"),
228 + use_tls: false,
229 + archive_folder_name: Some("All Mail"),
230 + },
221 231 )
222 232 .await
223 233 .expect("Failed to update account");
@@ -245,16 +255,18 @@
245 255 let created = repo
246 256 .create(
247 257 user_id,
248 - "My Account",
249 - "me@example.com",
250 - "imap.example.com",
251 - 993,
252 - "smtp.example.com",
253 - 587,
254 - "me",
255 - "originalpass",
256 - true,
257 - None,
258 + NewEmailAccount {
259 + account_name: "My Account",
260 + email_address: "me@example.com",
261 + imap_server: "imap.example.com",
262 + imap_port: 993,
263 + smtp_server: "smtp.example.com",
264 + smtp_port: 587,
265 + username: "me",
266 + password: "originalpass",
267 + use_tls: true,
268 + archive_folder_name: None,
269 + },
258 270 )
259 271 .await
260 272 .expect("Failed to create account");
@@ -263,16 +275,18 @@
263 275 .update(
264 276 created.id,
265 277 user_id,
266 - "Renamed Account",
267 - "me@example.com",
268 - "imap.example.com",
269 - 993,
270 - "smtp.example.com",
271 - 587,
272 - "me",
273 - None,
274 - true,
275 - None,
278 + UpdateEmailAccount {
279 + account_name: "Renamed Account",
280 + email_address: "me@example.com",
281 + imap_server: "imap.example.com",
282 + imap_port: 993,
283 + smtp_server: "smtp.example.com",
284 + smtp_port: 587,
285 + username: "me",
286 + password: None,
287 + use_tls: true,
288 + archive_folder_name: None,
289 + },
276 290 )
277 291 .await
278 292 .expect("Failed to update account");
@@ -378,16 +392,18 @@
378 392 let created = repo
379 393 .create(
380 394 user_id,
381 - "Throwaway",
382 - "throw@example.com",
383 - "imap.example.com",
384 - 993,
385 - "smtp.example.com",
386 - 587,
387 - "throw",
388 - "pass",
389 - true,
390 - None,
395 + NewEmailAccount {
396 + account_name: "Throwaway",
397 + email_address: "throw@example.com",
398 + imap_server: "imap.example.com",
399 + imap_port: 993,
400 + smtp_server: "smtp.example.com",
401 + smtp_port: 587,
402 + username: "throw",
403 + password: "pass",
404 + use_tls: true,
405 + archive_folder_name: None,
406 + },
391 407 )
392 408 .await
393 409 .expect("Failed to create account");
@@ -415,16 +431,18 @@
415 431 let created = repo
416 432 .create(
417 433 user1,
418 - "User1 Email",
419 - "user1@example.com",
420 - "imap.example.com",
421 - 993,
422 - "smtp.example.com",
423 - 587,
424 - "user1",
425 - "pass",
426 - true,
427 - None,
434 + NewEmailAccount {
435 + account_name: "User1 Email",
436 + email_address: "user1@example.com",
437 + imap_server: "imap.example.com",
438 + imap_port: 993,
439 + smtp_server: "smtp.example.com",
440 + smtp_port: 587,
441 + username: "user1",
442 + password: "pass",
443 + use_tls: true,
444 + archive_folder_name: None,
445 + },
428 446 )
429 447 .await
430 448 .expect("Failed to create account");
@@ -453,16 +471,18 @@
453 471 let created = repo
454 472 .create(
455 473 user_id,
456 - "Sync Test",
457 - "sync@example.com",
458 - "imap.example.com",
459 - 993,
460 - "smtp.example.com",
461 - 587,
462 - "sync",
463 - "pass",
464 - true,
465 - None,
474 + NewEmailAccount {
475 + account_name: "Sync Test",
476 + email_address: "sync@example.com",
477 + imap_server: "imap.example.com",
478 + imap_port: 993,
479 + smtp_server: "smtp.example.com",
480 + smtp_port: 587,
481 + username: "sync",
482 + password: "pass",
483 + use_tls: true,
484 + archive_folder_name: None,
485 + },
466 486 )
467 487 .await
468 488 .expect("Failed to create account");
@@ -492,16 +512,18 @@
492 512 let created = repo
493 513 .create(
494 514 user_id,
495 - "Interval Test",
496 - "interval@example.com",
497 - "imap.example.com",
498 - 993,
499 - "smtp.example.com",
500 - 587,
501 - "interval",
502 - "pass",
503 - true,
504 - None,
515 + NewEmailAccount {
516 + account_name: "Interval Test",
517 + email_address: "interval@example.com",
518 + imap_server: "imap.example.com",
519 + imap_port: 993,
520 + smtp_server: "smtp.example.com",
521 + smtp_port: 587,
522 + username: "interval",
523 + password: "pass",
524 + use_tls: true,
525 + archive_folder_name: None,
526 + },
505 527 )
506 528 .await
507 529 .expect("Failed to create account");
@@ -537,16 +559,18 @@
537 559 let account = repo
538 560 .create(
539 561 user_id,
540 - "Auto Sync",
541 - "autosync@example.com",
542 - "imap.example.com",
543 - 993,
544 - "smtp.example.com",
545 - 587,
546 - "autosync",
547 - "pass",
548 - true,
549 - None,
562 + NewEmailAccount {
563 + account_name: "Auto Sync",
564 + email_address: "autosync@example.com",
565 + imap_server: "imap.example.com",
566 + imap_port: 993,
567 + smtp_server: "smtp.example.com",
568 + smtp_port: 587,
569 + username: "autosync",
570 + password: "pass",
571 + use_tls: true,
572 + archive_folder_name: None,
573 + },
550 574 )
551 575 .await
552 576 .expect("Failed to create account");
@@ -9,7 +9,9 @@
9 9
10 10 use chrono::{Duration, Utc};
11 11 use goingson_core::email_sync::{FetchedEmail, process_fetched_emails};
12 - use goingson_core::{EmailAccountId, EmailAccountRepository, EmailRepository, UserId};
12 + use goingson_core::{
13 + EmailAccountId, EmailAccountRepository, EmailRepository, NewEmailAccount, UserId,
14 + };
13 15 use goingson_db_sqlite::{SqliteEmailAccountRepository, SqliteEmailRepository};
14 16 use sqlx::SqlitePool;
15 17
@@ -18,16 +20,18 @@
18 20 let account = repo
19 21 .create(
20 22 user_id,
21 - "Test",
22 - "me@example.com",
23 - "imap.example.com",
24 - 993,
25 - "smtp.example.com",
26 - 465,
27 - "me@example.com",
28 - "pw",
29 - true,
30 - Some("Archive"),
23 + NewEmailAccount {
24 + account_name: "Test",
25 + email_address: "me@example.com",
26 + imap_server: "imap.example.com",
27 + imap_port: 993,
28 + smtp_server: "smtp.example.com",
29 + smtp_port: 465,
30 + username: "me@example.com",
31 + password: "pw",
32 + use_tls: true,
33 + archive_folder_name: Some("Archive"),
34 + },
31 35 )
32 36 .await
33 37 .expect("create account");
@@ -12,7 +12,9 @@
12 12
13 13 use tracing::instrument;
14 14
15 - use goingson_core::{EmailAccount, EmailAccountId, EmailAuthType};
15 + use goingson_core::{
16 + EmailAccount, EmailAccountId, EmailAuthType, NewEmailAccount, UpdateEmailAccount,
17 + };
16 18
17 19 use super::{ApiError, OptionApiError, OptionNotFound, ResultApiError};
18 20 use crate::email::{ImapClient, SmtpClient, uses_jmap};
@@ -297,16 +299,18 @@
297 299 .email_accounts
298 300 .create(
299 301 DESKTOP_USER_ID,
300 - &input.account_name,
301 - &input.email_address,
302 - &input.imap_server,
303 - input.imap_port,
304 - &input.smtp_server,
305 - input.smtp_port,
306 - &input.username,
307 - "",
308 - input.use_tls,
309 - input.archive_folder_name.as_deref(),
302 + NewEmailAccount {
303 + account_name: &input.account_name,
304 + email_address: &input.email_address,
305 + imap_server: &input.imap_server,
306 + imap_port: input.imap_port,
307 + smtp_server: &input.smtp_server,
308 + smtp_port: input.smtp_port,
309 + username: &input.username,
310 + password: "",
311 + use_tls: input.use_tls,
312 + archive_folder_name: input.archive_folder_name.as_deref(),
313 + },
310 314 )
311 315 .await?;
312 316
@@ -367,16 +371,18 @@
367 371 .update(
368 372 id,
369 373 DESKTOP_USER_ID,
370 - &input.account_name,
371 - &input.email_address,
372 - &input.imap_server,
373 - input.imap_port,
374 - &input.smtp_server,
375 - input.smtp_port,
376 - &input.username,
377 - db_password,
378 - input.use_tls,
379 - input.archive_folder_name.as_deref(),
374 + UpdateEmailAccount {
375 + account_name: &input.account_name,
376 + email_address: &input.email_address,
377 + imap_server: &input.imap_server,
378 + imap_port: input.imap_port,
379 + smtp_server: &input.smtp_server,
380 + smtp_port: input.smtp_port,
381 + username: &input.username,
382 + password: db_password,
383 + use_tls: input.use_tls,
384 + archive_folder_name: input.archive_folder_name.as_deref(),
385 + },
380 386 )
381 387 .await?
382 388 .or_not_found("emailAccount", id)
@@ -165,6 +165,45 @@
165 165 pub notify_new_emails: bool,
166 166 }
167 167
168 + /// Fields needed to create a password-auth IMAP/SMTP account.
169 + ///
170 + /// Named rather than positional: the flat form was eleven arguments with two
171 + /// adjacent `&str` server pairs and an adjacent username/password pair, all
172 + /// silently swappable at a call site.
173 + #[derive(Debug, Clone)]
174 + pub struct NewEmailAccount<'a> {
175 + pub account_name: &'a str,
176 + pub email_address: &'a str,
177 + pub imap_server: &'a str,
178 + pub imap_port: i32,
179 + pub smtp_server: &'a str,
180 + pub smtp_port: i32,
181 + pub username: &'a str,
182 + /// Password to write to the DB column. Callers store the real secret in the
183 + /// OS keychain and pass `""` here.
184 + pub password: &'a str,
185 + pub use_tls: bool,
186 + pub archive_folder_name: Option<&'a str>,
187 + }
188 +
189 + /// Fields needed to update a password-auth IMAP/SMTP account.
190 + ///
191 + /// `password` is tri-state: `None` leaves the column untouched, `Some("")`
192 + /// clears any stale plaintext, `Some(pwd)` writes it.
193 + #[derive(Debug, Clone)]
194 + pub struct UpdateEmailAccount<'a> {
195 + pub account_name: &'a str,
196 + pub email_address: &'a str,
197 + pub imap_server: &'a str,
198 + pub imap_port: i32,
199 + pub smtp_server: &'a str,
200 + pub smtp_port: i32,
201 + pub username: &'a str,
202 + pub password: Option<&'a str>,
203 + pub use_tls: bool,
204 + pub archive_folder_name: Option<&'a str>,
205 + }
206 +
168 207 /// Per-folder IMAP sync state for incremental UID-based fetching.
169 208 #[derive(Debug, Clone)]
170 209 pub struct FolderSyncState {
@@ -1,7 +1,7 @@
1 1 use super::{
2 2 Attachment, AttachmentId, DateTime, Email, EmailAccount, EmailAccountId, EmailAuthType,
3 - EmailId, EmailThread, FolderSyncState, HashSet, NewAttachment, NewEmail, NewEmailWithTracking,
4 - ProjectId, Result, TaskId, UserId, Utc, async_trait,
3 + EmailId, EmailThread, FolderSyncState, HashSet, NewAttachment, NewEmail, NewEmailAccount,
4 + NewEmailWithTracking, ProjectId, Result, TaskId, UpdateEmailAccount, UserId, Utc, async_trait,
5 5 };
6 6
7 7 /// Repository for email message operations.
@@ -213,20 +213,7 @@
213 213 async fn get_by_id(&self, id: EmailAccountId, user_id: UserId) -> Result<Option<EmailAccount>>;
214 214
215 215 /// Creates a new email account configuration (password-based IMAP/SMTP).
216 - async fn create(
217 - &self,
218 - user_id: UserId,
219 - account_name: &str,
220 - email_address: &str,
221 - imap_server: &str,
222 - imap_port: i32,
223 - smtp_server: &str,
224 - smtp_port: i32,
225 - username: &str,
226 - password: &str,
227 - use_tls: bool,
228 - archive_folder_name: Option<&str>,
229 - ) -> Result<EmailAccount>;
216 + async fn create(&self, user_id: UserId, account: NewEmailAccount<'_>) -> Result<EmailAccount>;
230 217
231 218 /// Creates a new OAuth2 email account (Fastmail JMAP).
232 219 async fn create_oauth(
@@ -262,16 +249,7 @@
262 249 &self,
263 250 id: EmailAccountId,
264 251 user_id: UserId,
265 - account_name: &str,
266 - email_address: &str,
267 - imap_server: &str,
268 - imap_port: i32,
269 - smtp_server: &str,
270 - smtp_port: i32,
271 - username: &str,
272 - password: Option<&str>,
273 - use_tls: bool,
274 - archive_folder_name: Option<&str>,
252 + account: UpdateEmailAccount<'_>,
275 253 ) -> Result<Option<EmailAccount>>;
276 254
277 255 /// Updates OAuth2 tokens for an account.
@@ -21,10 +21,10 @@
21 21 use crate::error::CoreError;
22 22 use crate::models::{
23 23 Annotation, Attachment, Email, EmailAccount, EmailAuthType, EmailThread, Event,
24 - FolderSyncState, NewAttachment, NewEmail, NewEmailWithTracking, NewEvent, NewProblem,
25 - NewProject, NewSavedView, NewTask, PositiveMinutes, Problem, ProblemStatus, Project, SavedView,
26 - StatusToken, Subtask, Task, TaskFilterQuery, TimeSession, TimeTrackingSummary, TokenState,
27 - UpdateTask, User,
24 + FolderSyncState, NewAttachment, NewEmail, NewEmailAccount, NewEmailWithTracking, NewEvent,
25 + NewProblem, NewProject, NewSavedView, NewTask, PositiveMinutes, Problem, ProblemStatus,
26 + Project, SavedView, StatusToken, Subtask, Task, TaskFilterQuery, TimeSession,
27 + TimeTrackingSummary, TokenState, UpdateEmailAccount, UpdateTask, User,
28 28 };
29 29
30 30 /// Convenience type alias for repository operation results.
@@ -7,7 +7,7 @@
7 7 use chrono::{DateTime, Utc};
8 8 use goingson_core::{
9 9 CoreError, EmailAccount, EmailAccountId, EmailAccountRepository, EmailAuthType,
10 - FolderSyncState, Result, UserId,
10 + FolderSyncState, NewEmailAccount, Result, UpdateEmailAccount, UserId,
11 11 };
12 12 use sqlx::SqlitePool;
13 13
@@ -135,20 +135,19 @@
135 135 }
136 136
137 137 #[tracing::instrument(skip_all)]
138 - async fn create(
139 - &self,
140 - user_id: UserId,
141 - account_name: &str,
142 - email_address: &str,
143 - imap_server: &str,
144 - imap_port: i32,
145 - smtp_server: &str,
146 - smtp_port: i32,
147 - username: &str,
148 - password: &str,
149 - use_tls: bool,
150 - archive_folder_name: Option<&str>,
151 - ) -> Result<EmailAccount> {
138 + async fn create(&self, user_id: UserId, account: NewEmailAccount<'_>) -> Result<EmailAccount> {
139 + let NewEmailAccount {
140 + account_name,
141 + email_address,
142 + imap_server,
143 + imap_port,
144 + smtp_server,
145 + smtp_port,
146 + username,
147 + password,
148 + use_tls,
149 + archive_folder_name,
150 + } = account;
152 151 let id = EmailAccountId::new();
153 152 let now = format_datetime_now();
154 153 sqlx::query(r"
@@ -251,17 +250,20 @@
251 250 &self,
252 251 id: EmailAccountId,
253 252 user_id: UserId,
254 - account_name: &str,
255 - email_address: &str,
256 - imap_server: &str,
257 - imap_port: i32,
258 - smtp_server: &str,
259 - smtp_port: i32,
260 - username: &str,
261 - password: Option<&str>,
262 - use_tls: bool,
263 - archive_folder_name: Option<&str>,
253 + account: UpdateEmailAccount<'_>,
264 254 ) -> Result<Option<EmailAccount>> {
255 + let UpdateEmailAccount {
256 + account_name,
257 + email_address,
258 + imap_server,
259 + imap_port,
260 + smtp_server,
261 + smtp_port,
262 + username,
263 + password,
264 + use_tls,
265 + archive_folder_name,
266 + } = account;
265 267 let result = if let Some(pwd) = password {
266 268 sqlx::query(r"
267 269 UPDATE email_accounts SET account_name = ?, email_address = ?, imap_server = ?, imap_port = ?,