@@ -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+
180221export 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