@@ -313,23 +313,39 @@ pub(super) fn guess_content_type(filename: &str) -> &'static str {
313313 }
314314}
315315
316- /// Read attachment files from a comma-separated list of paths.
317- pub ( super ) fn read_attachments ( paths_csv : & str ) -> Result < Vec < Attachment > , GwsError > {
316+ /// Read attachment files from a list of paths.
317+ ///
318+ /// Each path is canonicalized to resolve symlinks and `..` components; the
319+ /// function rejects paths that do not point to a regular file.
320+ pub ( super ) fn read_attachments ( paths : & [ String ] ) -> Result < Vec < Attachment > , GwsError > {
318321 let mut attachments = Vec :: new ( ) ;
319- for path_str in paths_csv . split ( ',' ) {
322+ for path_str in paths {
320323 let path_str = path_str. trim ( ) ;
321324 if path_str. is_empty ( ) {
322325 continue ;
323326 }
324327 let path = std:: path:: Path :: new ( path_str) ;
325- let data = std:: fs:: read ( path) . map_err ( |e| {
328+ let canonical = path. canonicalize ( ) . map_err ( |e| {
329+ GwsError :: Other ( anyhow:: anyhow!(
330+ "Failed to resolve attachment path '{}': {}" ,
331+ path_str,
332+ e
333+ ) )
334+ } ) ?;
335+ if !canonical. is_file ( ) {
336+ return Err ( GwsError :: Other ( anyhow:: anyhow!(
337+ "Attachment path '{}' is not a regular file" ,
338+ path_str,
339+ ) ) ) ;
340+ }
341+ let data = std:: fs:: read ( & canonical) . map_err ( |e| {
326342 GwsError :: Other ( anyhow:: anyhow!(
327343 "Failed to read attachment '{}': {}" ,
328344 path_str,
329345 e
330346 ) )
331347 } ) ?;
332- let filename = path
348+ let filename = canonical
333349 . file_name ( )
334350 . and_then ( |n| n. to_str ( ) )
335351 . unwrap_or ( path_str)
@@ -456,12 +472,17 @@ impl MessageBuilder<'_> {
456472 for att in attachments {
457473 let encoded = STANDARD . encode ( & att. data ) ;
458474 // Fold base64 into 76-character lines per RFC 2045.
459- let folded: String = encoded
460- . as_bytes ( )
461- . chunks ( 76 )
462- . map ( |chunk| std:: str:: from_utf8 ( chunk) . unwrap ( ) )
463- . collect :: < Vec < _ > > ( )
464- . join ( "\r \n " ) ;
475+ // Base64 output is pure ASCII, so byte-index slicing is safe.
476+ let mut folded = String :: with_capacity ( encoded. len ( ) + ( encoded. len ( ) / 76 ) * 2 ) ;
477+ let mut offset = 0 ;
478+ while offset < encoded. len ( ) {
479+ if offset > 0 {
480+ folded. push_str ( "\r \n " ) ;
481+ }
482+ let end = ( offset + 76 ) . min ( encoded. len ( ) ) ;
483+ folded. push_str ( & encoded[ offset..end] ) ;
484+ offset = end;
485+ }
465486
466487 let safe_filename = escape_quoted_string ( & att. filename ) ;
467488 message. push_str ( & format ! (
@@ -630,8 +651,9 @@ impl Helper for GmailHelper {
630651 . arg (
631652 Arg :: new ( "attachment" )
632653 . long ( "attachment" )
633- . help ( "File path(s) to attach, comma-separated" )
634- . value_name ( "PATHS" ) ,
654+ . help ( "File path to attach (may be repeated)" )
655+ . value_name ( "PATH" )
656+ . action ( ArgAction :: Append ) ,
635657 )
636658 . arg (
637659 Arg :: new ( "dry-run" )
@@ -646,7 +668,7 @@ EXAMPLES:
646668 gws gmail +send --to alice@example.com --subject 'Hello' --body 'Hi!' --cc bob@example.com
647669 gws gmail +send --to alice@example.com --subject 'Hello' --body 'Hi!' --bcc secret@example.com
648670 gws gmail +send --to alice@example.com --subject 'Report' --body 'See attached' --attachment report.pdf
649- gws gmail +send --to alice@example.com --subject 'Files' --body 'Multiple' --attachment ' a.pdf, b.zip'
671+ gws gmail +send --to alice@example.com --subject 'Files' --body 'Multiple' --attachment a.pdf --attachment b.zip
650672
651673TIPS:
652674 Handles RFC 2822 formatting and base64 encoding automatically.
@@ -734,8 +756,9 @@ TIPS:
734756 . arg (
735757 Arg :: new ( "attachment" )
736758 . long ( "attachment" )
737- . help ( "File path(s) to attach, comma-separated" )
738- . value_name ( "PATHS" ) ,
759+ . help ( "File path to attach (may be repeated)" )
760+ . value_name ( "PATH" )
761+ . action ( ArgAction :: Append ) ,
739762 )
740763 . arg (
741764 Arg :: new ( "dry-run" )
@@ -803,8 +826,9 @@ TIPS:
803826 . arg (
804827 Arg :: new ( "attachment" )
805828 . long ( "attachment" )
806- . help ( "File path(s) to attach, comma-separated" )
807- . value_name ( "PATHS" ) ,
829+ . help ( "File path to attach (may be repeated)" )
830+ . value_name ( "PATH" )
831+ . action ( ArgAction :: Append ) ,
808832 )
809833 . arg (
810834 Arg :: new ( "remove" )
@@ -881,8 +905,9 @@ TIPS:
881905 . arg (
882906 Arg :: new ( "attachment" )
883907 . long ( "attachment" )
884- . help ( "File path(s) to attach, comma-separated" )
885- . value_name ( "PATHS" ) ,
908+ . help ( "File path to attach (may be repeated)" )
909+ . value_name ( "PATH" )
910+ . action ( ArgAction :: Append ) ,
886911 )
887912 . arg (
888913 Arg :: new ( "dry-run" )
0 commit comments