Skip to content

Commit e78684b

Browse files
plossonclaude
andcommitted
feat(gmail): add 'filters create' command
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 259f95d commit e78684b

1 file changed

Lines changed: 112 additions & 0 deletions

File tree

src/commands/gmail.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,47 @@ async function buildLabelNamesById(client: GmailClient): Promise<Map<string, str
177177
return new Map(labels.map((l) => [l.id, l.name]));
178178
}
179179

180+
function parseFilterCriteriaFromOptions(options: Record<string, unknown>): GmailFilterCriteria {
181+
const criteria: GmailFilterCriteria = {};
182+
183+
const from = options.from as string | undefined;
184+
const to = options.to as string | undefined;
185+
const subject = options.subject as string | undefined;
186+
const query = options.query as string | undefined;
187+
const negatedQuery = options.negatedQuery as string | undefined;
188+
const hasAttachment = options.hasAttachment === true;
189+
const excludeChats = options.excludeChats === true;
190+
const sizeRaw = options.size as string | undefined;
191+
const sizeComparison = options.sizeComparison as string | undefined;
192+
193+
if (from) criteria.from = from;
194+
if (to) criteria.to = to;
195+
if (subject) criteria.subject = subject;
196+
if (query) criteria.query = query;
197+
if (negatedQuery) criteria.negatedQuery = negatedQuery;
198+
if (hasAttachment) criteria.hasAttachment = true;
199+
if (excludeChats) criteria.excludeChats = true;
200+
201+
const sizeProvided = sizeRaw !== undefined;
202+
const cmpProvided = sizeComparison !== undefined;
203+
if (sizeProvided !== cmpProvided) {
204+
throw new CliError('INVALID_PARAMS', '--size and --size-comparison must be set together');
205+
}
206+
if (sizeProvided && cmpProvided) {
207+
if (sizeComparison !== 'larger' && sizeComparison !== 'smaller') {
208+
throw new CliError('INVALID_PARAMS', '--size-comparison must be "larger" or "smaller"');
209+
}
210+
const sizeNum = parseInt(sizeRaw!, 10);
211+
if (!Number.isFinite(sizeNum) || sizeNum < 0) {
212+
throw new CliError('INVALID_PARAMS', '--size must be a non-negative integer (bytes)');
213+
}
214+
criteria.size = sizeNum;
215+
criteria.sizeComparison = sizeComparison;
216+
}
217+
218+
return criteria;
219+
}
220+
180221
export function registerGmailCommands(program: Command): void {
181222
const gmail = program
182223
.command('gmail')
@@ -618,6 +659,77 @@ Combine with spaces (AND), OR, or - to negate.`,
618659
agentio gmail filters get ANe1BmgABCDEF1234567890`,
619660
);
620661

662+
addExamples(
663+
filters
664+
.command('create')
665+
.description('Create a Gmail filter')
666+
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
667+
.option('--from <email>', 'Match sender')
668+
.option('--to <email>', 'Match recipient')
669+
.option('--subject <text>', 'Match subject text')
670+
.option('--query <q>', 'Gmail search query (same syntax as "gmail search")')
671+
.option('--negated-query <q>', 'Gmail search query that must NOT match')
672+
.option('--has-attachment', 'Match only messages with attachments')
673+
.option('--exclude-chats', 'Exclude chat messages')
674+
.option('--size <bytes>', 'Match by message size (paired with --size-comparison)')
675+
.option('--size-comparison <cmp>', 'Size comparison: larger|smaller (paired with --size)')
676+
.option('--apply <label>', 'Label to apply (name or ID, repeatable)', (val: string, acc: string[]) => [...acc, val], [])
677+
.option('--remove <label>', 'Label to remove (name or ID, repeatable)', (val: string, acc: string[]) => [...acc, val], [])
678+
.option('--forward <email>', 'Forward to a verified forwarding address')
679+
.action(async (options) => {
680+
try {
681+
const criteria = parseFilterCriteriaFromOptions(options);
682+
if (Object.keys(criteria).length === 0) {
683+
throw new CliError('INVALID_PARAMS', 'At least one criterion is required', 'Use --from, --to, --subject, --query, --negated-query, --has-attachment, --exclude-chats, or --size');
684+
}
685+
686+
const apply = options.apply as string[];
687+
const remove = options.remove as string[];
688+
const forward = options.forward as string | undefined;
689+
if (!apply.length && !remove.length && !forward) {
690+
throw new CliError('INVALID_PARAMS', 'At least one action is required', 'Use --apply, --remove, or --forward');
691+
}
692+
693+
const { client, profile } = await getGmailClient(options.profile);
694+
await enforceWriteAccess('gmail', profile, 'create filter');
695+
696+
const [addLabelIds, removeLabelIds] = await Promise.all([
697+
client.resolveLabelIds(apply),
698+
client.resolveLabelIds(remove),
699+
]);
700+
701+
const action: GmailFilterAction = {};
702+
if (addLabelIds.length) action.addLabelIds = addLabelIds;
703+
if (removeLabelIds.length) action.removeLabelIds = removeLabelIds;
704+
if (forward) action.forward = forward;
705+
706+
const filter = await client.createFilter({ criteria, action });
707+
const labelNamesById = await buildLabelNamesById(client);
708+
printFilterCreated(filter, labelNamesById);
709+
} catch (error) {
710+
handleError(error);
711+
}
712+
}),
713+
`Examples:
714+
715+
# apply a label to mail from a sender
716+
agentio gmail filters create --from noreply@example.com --apply Receipts
717+
718+
# archive newsletters automatically
719+
agentio gmail filters create --from news@example.com --remove INBOX
720+
721+
# complex criteria + multiple actions
722+
agentio gmail filters create \\
723+
--query "has:attachment subject:invoice" \\
724+
--apply Auto/Invoices --remove INBOX
725+
726+
# forward all mail from a sender (forwarding address must be verified in Gmail settings)
727+
agentio gmail filters create --from boss@example.com --forward archive@me.com
728+
729+
# size-based filter (5MB or larger)
730+
agentio gmail filters create --size 5000000 --size-comparison larger --apply Large`,
731+
);
732+
621733
addExamples(
622734
gmail
623735
.command('label')

0 commit comments

Comments
 (0)