Merge pull request #650 from rowboatlabs/mail-enhancements

feat(email): drafts, search, read-state controls & Superhuman-style shortcuts
This commit is contained in:
arkml 2026-07-03 01:12:40 +05:30 committed by GitHub
commit 3ba94402d3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 2064 additions and 259 deletions

View file

@ -107,6 +107,10 @@ export const GmailAttachmentSchema = z.object({
mimeType: z.string().optional(),
sizeBytes: z.number().int().nonnegative().optional(),
savedPath: z.string(),
// Gmail identifiers used to fetch the attachment on demand when it hasn't
// been downloaded to disk yet (e.g. attachments on search results).
messageId: z.string().optional(),
attachmentId: z.string().optional(),
});
export type GmailAttachment = z.infer<typeof GmailAttachmentSchema>;
@ -124,6 +128,14 @@ export const GmailThreadMessageSchema = z.object({
bodyHeight: z.number().int().positive().optional(),
attachments: z.array(GmailAttachmentSchema).optional(),
messageIdHeader: z.string().optional(),
// Set on the unsent draft message within a thread (used by the Drafts view).
isDraft: z.boolean().optional(),
// The draft's own stored In-Reply-To / References headers. Only set on
// draft messages: a Drafts-view pseudo-thread contains just the draft, so
// the composer can't rebuild the reply chain from thread messages and must
// reuse what the draft already carries.
inReplyToHeader: z.string().optional(),
referencesHeader: z.string().optional(),
});
export type GmailThreadMessage = z.infer<typeof GmailThreadMessageSchema>;
@ -134,6 +146,9 @@ export const GmailThreadSchema = EmailBlockSchema.extend({
unread: z.boolean().optional(),
importance: z.enum(['important', 'other']).optional(),
gmail_draft: z.string().optional(),
// Gmail-side draft id, present on entries returned by the Drafts list so the
// composer can update/delete that exact draft.
draftId: z.string().optional(),
messages: z.array(GmailThreadMessageSchema),
});

View file

@ -207,6 +207,56 @@ const ipcSchemas = {
error: z.string().optional(),
}),
},
'gmail:saveDraft': {
req: z.object({
// Existing Gmail draft to update; omitted on first save (creates a new one).
draftId: z.string().min(1).optional(),
threadId: z.string().min(1).optional(),
// Recipients may be blank for a draft (unlike a send).
to: z.string().optional(),
cc: z.string().optional(),
bcc: z.string().optional(),
subject: z.string(),
bodyHtml: z.string(),
bodyText: z.string(),
inReplyTo: z.string().optional(),
references: z.string().optional(),
attachments: z
.array(
z.object({
filename: z.string(),
mimeType: z.string(),
contentBase64: z.string(),
}),
)
.optional(),
}),
res: z.object({
draftId: z.string().optional(),
error: z.string().optional(),
}),
},
'gmail:deleteDraft': {
req: z.object({ draftId: z.string().min(1) }),
res: z.object({ ok: z.boolean(), error: z.string().optional() }),
},
'gmail:getDrafts': {
req: z.object({}),
res: z.object({
threads: z.array(GmailThreadSchema),
error: z.string().optional(),
}),
},
'gmail:search': {
req: z.object({
query: z.string(),
limit: z.number().int().positive().optional(),
}),
res: z.object({
threads: z.array(GmailThreadSchema),
error: z.string().optional(),
}),
},
'gmail:getConnectionStatus': {
req: z.object({}),
res: z.object({
@ -237,7 +287,15 @@ const ipcSchemas = {
res: z.object({ ok: z.boolean(), error: z.string().optional() }),
},
'gmail:markThreadRead': {
req: z.object({ threadId: z.string().min(1) }),
req: z.object({ threadId: z.string().min(1), read: z.boolean().optional() }),
res: z.object({ ok: z.boolean(), error: z.string().optional() }),
},
'gmail:downloadAttachment': {
req: z.object({
messageId: z.string().min(1),
savedPath: z.string().min(1),
attachmentId: z.string().optional(),
}),
res: z.object({ ok: z.boolean(), error: z.string().optional() }),
},
'gmail:saveMessageHeight': {