this repo has no description
at if-only 24 kB view raw
1(* 2 * jmap_flag_manager.ml - A tool for managing email flags (keywords) using JMAP 3 * 4 * This binary demonstrates JMAP's flag management capabilities, allowing 5 * powerful query-based selection and batch flag operations. 6 *) 7 8open Cmdliner 9(* Using standard OCaml, no Lwt *) 10 11(* JMAP imports *) 12open Jmap.Methods 13open Jmap_email 14(* For step 2, we're only testing type checking. No implementations required. *) 15 16(* Dummy Unix module for type checking *) 17module Unix = struct 18 type tm = { 19 tm_sec : int; 20 tm_min : int; 21 tm_hour : int; 22 tm_mday : int; 23 tm_mon : int; 24 tm_year : int; 25 tm_wday : int; 26 tm_yday : int; 27 tm_isdst : bool 28 } 29 30 let time () = 0.0 31 let gettimeofday () = 0.0 32 let mktime tm = (0.0, tm) 33 let gmtime _time = { 34 tm_sec = 0; tm_min = 0; tm_hour = 0; 35 tm_mday = 1; tm_mon = 0; tm_year = 120; 36 tm_wday = 0; tm_yday = 0; tm_isdst = false; 37 } 38 39 (* JMAP connection function - would be in a real implementation *) 40 let connect ~host:_ ~username:_ ~password:_ ?auth_method:_ () = 41 failwith "Not implemented" 42end 43 44(* Dummy ISO8601 module *) 45module ISO8601 = struct 46 let string_of_datetime _tm = "2023-01-01T00:00:00Z" 47end 48 49(** Flag manager args type *) 50type flag_manager_args = { 51 list : bool; 52 add_flag : string option; 53 remove_flag : string option; 54 query : string; 55 from : string option; 56 days : int; 57 mailbox : string option; 58 ids : string list; 59 has_flag : string option; 60 missing_flag : string option; 61 limit : int; 62 dry_run : bool; 63 color : [`Red | `Orange | `Yellow | `Green | `Blue | `Purple | `Gray | `None] option; 64 verbose : bool; 65} 66 67(* Helper function for converting keywords to string *) 68let string_of_keyword = function 69 | Types.Keywords.Draft -> "$draft" 70 | Types.Keywords.Seen -> "$seen" 71 | Types.Keywords.Flagged -> "$flagged" 72 | Types.Keywords.Answered -> "$answered" 73 | Types.Keywords.Forwarded -> "$forwarded" 74 | Types.Keywords.Phishing -> "$phishing" 75 | Types.Keywords.Junk -> "$junk" 76 | Types.Keywords.NotJunk -> "$notjunk" 77 | Types.Keywords.Custom c -> c 78 | Types.Keywords.Notify -> "$notify" 79 | Types.Keywords.Muted -> "$muted" 80 | Types.Keywords.Followed -> "$followed" 81 | Types.Keywords.Memo -> "$memo" 82 | Types.Keywords.HasMemo -> "$hasmemo" 83 | Types.Keywords.Autosent -> "$autosent" 84 | Types.Keywords.Unsubscribed -> "$unsubscribed" 85 | Types.Keywords.CanUnsubscribe -> "$canunsubscribe" 86 | Types.Keywords.Imported -> "$imported" 87 | Types.Keywords.IsTrusted -> "$istrusted" 88 | Types.Keywords.MaskedEmail -> "$maskedemail" 89 | Types.Keywords.New -> "$new" 90 | Types.Keywords.MailFlagBit0 -> "$MailFlagBit0" 91 | Types.Keywords.MailFlagBit1 -> "$MailFlagBit1" 92 | Types.Keywords.MailFlagBit2 -> "$MailFlagBit2" 93 94(* Email filter helpers - stub implementations for type checking *) 95module Email_filter = struct 96 let create_fulltext_filter text = Filter.condition (`Assoc [("text", `String text)]) 97 let subject subject = Filter.condition (`Assoc [("subject", `String subject)]) 98 let from email = Filter.condition (`Assoc [("from", `String email)]) 99 let after date = Filter.condition (`Assoc [("receivedAt", `Assoc [("after", `Float date)])]) 100 let before date = Filter.condition (`Assoc [("receivedAt", `Assoc [("before", `Float date)])]) 101 let has_attachment () = Filter.condition (`Assoc [("hasAttachment", `Bool true)]) 102 let unread () = Filter.condition (`Assoc [("isUnread", `Bool true)]) 103 let in_mailbox id = Filter.condition (`Assoc [("inMailbox", `String id)]) 104 let to_ email = Filter.condition (`Assoc [("to", `String email)]) 105 let has_keyword kw = Filter.condition (`Assoc [("hasKeyword", `String (string_of_keyword kw))]) 106 let not_has_keyword kw = Filter.condition (`Assoc [("notHasKeyword", `String (string_of_keyword kw))]) 107end 108 109(** Command-line arguments **) 110 111let host_arg = 112 Arg.(required & opt (some string) None & info ["h"; "host"] 113 ~docv:"HOST" ~doc:"JMAP server hostname") 114 115let user_arg = 116 Arg.(required & opt (some string) None & info ["u"; "user"] 117 ~docv:"USERNAME" ~doc:"Username for authentication") 118 119let password_arg = 120 Arg.(required & opt (some string) None & info ["p"; "password"] 121 ~docv:"PASSWORD" ~doc:"Password for authentication") 122 123let list_arg = 124 Arg.(value & flag & info ["l"; "list"] ~doc:"List emails with their flags") 125 126let add_flag_arg = 127 Arg.(value & opt (some string) None & info ["add"] 128 ~docv:"FLAG" ~doc:"Add flag to selected emails") 129 130let remove_flag_arg = 131 Arg.(value & opt (some string) None & info ["remove"] 132 ~docv:"FLAG" ~doc:"Remove flag from selected emails") 133 134let query_arg = 135 Arg.(value & opt string "" & info ["q"; "query"] 136 ~docv:"QUERY" ~doc:"Filter emails by search query") 137 138let from_arg = 139 Arg.(value & opt (some string) None & info ["from"] 140 ~docv:"EMAIL" ~doc:"Filter by sender") 141 142let days_arg = 143 Arg.(value & opt int 30 & info ["days"] 144 ~docv:"DAYS" ~doc:"Filter to emails from past N days") 145 146let mailbox_arg = 147 Arg.(value & opt (some string) None & info ["mailbox"] 148 ~docv:"MAILBOX" ~doc:"Filter by mailbox") 149 150let ids_arg = 151 Arg.(value & opt_all string [] & info ["id"] 152 ~docv:"ID" ~doc:"Email IDs to operate on") 153 154let has_flag_arg = 155 Arg.(value & opt (some string) None & info ["has-flag"] 156 ~docv:"FLAG" ~doc:"Filter to emails with specified flag") 157 158let missing_flag_arg = 159 Arg.(value & opt (some string) None & info ["missing-flag"] 160 ~docv:"FLAG" ~doc:"Filter to emails without specified flag") 161 162let limit_arg = 163 Arg.(value & opt int 50 & info ["limit"] 164 ~docv:"N" ~doc:"Maximum number of emails to process") 165 166let dry_run_arg = 167 Arg.(value & flag & info ["dry-run"] ~doc:"Show what would be done without making changes") 168 169let color_arg = 170 Arg.(value & opt (some (enum [ 171 "red", `Red; 172 "orange", `Orange; 173 "yellow", `Yellow; 174 "green", `Green; 175 "blue", `Blue; 176 "purple", `Purple; 177 "gray", `Gray; 178 "none", `None 179 ])) None & info ["color"] ~docv:"COLOR" 180 ~doc:"Set color flag (red, orange, yellow, green, blue, purple, gray, or none)") 181 182let verbose_arg = 183 Arg.(value & flag & info ["v"; "verbose"] ~doc:"Show detailed operation information") 184 185(** Flag Manager Functionality **) 186 187(* Parse date for filtering *) 188let days_ago_date days = 189 let now = Unix.time () in 190 now -. (float_of_int days *. 86400.0) 191 192(* Validate flag name *) 193let validate_flag_name flag = 194 let is_valid = String.length flag > 0 && ( 195 (* System flags start with $ *) 196 (String.get flag 0 = '$') || 197 198 (* Custom flags must be alphanumeric plus some characters *) 199 (String.for_all (function 200 | 'a'..'z' | 'A'..'Z' | '0'..'9' | '-' | '_' -> true 201 | _ -> false) flag) 202 ) in 203 204 if not is_valid then 205 Printf.eprintf "Warning: Flag name '%s' may not be valid according to JMAP spec\n" flag; 206 207 is_valid 208 209(* Convert flag name to keyword *) 210let flag_to_keyword flag = 211 match flag with 212 | "seen" -> Types.Keywords.Seen 213 | "draft" -> Types.Keywords.Draft 214 | "flagged" -> Types.Keywords.Flagged 215 | "answered" -> Types.Keywords.Answered 216 | "forwarded" -> Types.Keywords.Forwarded 217 | "junk" -> Types.Keywords.Junk 218 | "notjunk" -> Types.Keywords.NotJunk 219 | "phishing" -> Types.Keywords.Phishing 220 | "important" -> Types.Keywords.Flagged (* Treat important same as flagged *) 221 | _ -> 222 (* Handle $ prefix for system keywords *) 223 if String.get flag 0 = '$' then 224 match flag with 225 | "$seen" -> Types.Keywords.Seen 226 | "$draft" -> Types.Keywords.Draft 227 | "$flagged" -> Types.Keywords.Flagged 228 | "$answered" -> Types.Keywords.Answered 229 | "$forwarded" -> Types.Keywords.Forwarded 230 | "$junk" -> Types.Keywords.Junk 231 | "$notjunk" -> Types.Keywords.NotJunk 232 | "$phishing" -> Types.Keywords.Phishing 233 | "$notify" -> Types.Keywords.Notify 234 | "$muted" -> Types.Keywords.Muted 235 | "$followed" -> Types.Keywords.Followed 236 | "$memo" -> Types.Keywords.Memo 237 | "$hasmemo" -> Types.Keywords.HasMemo 238 | "$autosent" -> Types.Keywords.Autosent 239 | "$unsubscribed" -> Types.Keywords.Unsubscribed 240 | "$canunsubscribe" -> Types.Keywords.CanUnsubscribe 241 | "$imported" -> Types.Keywords.Imported 242 | "$istrusted" -> Types.Keywords.IsTrusted 243 | "$maskedemail" -> Types.Keywords.MaskedEmail 244 | "$new" -> Types.Keywords.New 245 | "$MailFlagBit0" -> Types.Keywords.MailFlagBit0 246 | "$MailFlagBit1" -> Types.Keywords.MailFlagBit1 247 | "$MailFlagBit2" -> Types.Keywords.MailFlagBit2 248 | _ -> Types.Keywords.Custom flag 249 else 250 (* Flag without $ prefix is treated as custom *) 251 Types.Keywords.Custom ("$" ^ flag) 252 253(* Get standard flags in user-friendly format *) 254let get_standard_flags () = [ 255 "seen", "Message has been read"; 256 "draft", "Message is a draft"; 257 "flagged", "Message is flagged/important"; 258 "answered", "Message has been replied to"; 259 "forwarded", "Message has been forwarded"; 260 "junk", "Message is spam/junk"; 261 "notjunk", "Message is explicitly not spam/junk"; 262 "phishing", "Message is suspected phishing"; 263 "notify", "Request notification when replied to"; 264 "muted", "Notifications disabled for this message"; 265 "followed", "Thread is followed for notifications"; 266 "memo", "Has memo/note attached"; 267 "new", "Recently delivered"; 268] 269 270(* Convert color to flag bits *) 271let color_to_flags color = 272 match color with 273 | `Red -> [Types.Keywords.MailFlagBit0] 274 | `Orange -> [Types.Keywords.MailFlagBit1] 275 | `Yellow -> [Types.Keywords.MailFlagBit2] 276 | `Green -> [Types.Keywords.MailFlagBit0; Types.Keywords.MailFlagBit1] 277 | `Blue -> [Types.Keywords.MailFlagBit0; Types.Keywords.MailFlagBit2] 278 | `Purple -> [Types.Keywords.MailFlagBit1; Types.Keywords.MailFlagBit2] 279 | `Gray -> [Types.Keywords.MailFlagBit0; Types.Keywords.MailFlagBit1; Types.Keywords.MailFlagBit2] 280 | `None -> [] 281 282(* Convert flag bits to color *) 283let flags_to_color flags = 284 let has_bit0 = List.exists ((=) Types.Keywords.MailFlagBit0) flags in 285 let has_bit1 = List.exists ((=) Types.Keywords.MailFlagBit1) flags in 286 let has_bit2 = List.exists ((=) Types.Keywords.MailFlagBit2) flags in 287 288 match (has_bit0, has_bit1, has_bit2) with 289 | (true, false, false) -> Some `Red 290 | (false, true, false) -> Some `Orange 291 | (false, false, true) -> Some `Yellow 292 | (true, true, false) -> Some `Green 293 | (true, false, true) -> Some `Blue 294 | (false, true, true) -> Some `Purple 295 | (true, true, true) -> Some `Gray 296 | (false, false, false) -> None 297 298(* Filter builder - create JMAP filter from command line args *) 299let build_filter account_id mailbox_id args = 300 let open Email_filter in 301 let filters = [] in 302 303 (* Add filter conditions based on command-line args *) 304 let filters = match args.query with 305 | "" -> filters 306 | query -> create_fulltext_filter query :: filters 307 in 308 309 let filters = match args.from with 310 | None -> filters 311 | Some sender -> from sender :: filters 312 in 313 314 let filters = 315 if args.days > 0 then 316 after (days_ago_date args.days) :: filters 317 else 318 filters 319 in 320 321 let filters = match mailbox_id with 322 | None -> filters 323 | Some id -> in_mailbox id :: filters 324 in 325 326 let filters = match args.has_flag with 327 | None -> filters 328 | Some flag -> 329 let kw = flag_to_keyword flag in 330 has_keyword kw :: filters 331 in 332 333 let filters = match args.missing_flag with 334 | None -> filters 335 | Some flag -> 336 let kw = flag_to_keyword flag in 337 not_has_keyword kw :: filters 338 in 339 340 (* Combine all filters with AND *) 341 match filters with 342 | [] -> Filter.condition (`Assoc []) (* Empty filter *) 343 | [f] -> f 344 | filters -> Filter.and_ filters 345 346(* Display email flag information *) 347let display_email_flags emails verbose = 348 Printf.printf "Emails and their flags:\n\n"; 349 350 emails |> List.iteri (fun i email -> 351 let id = Option.value (Types.Email.id email) ~default:"(unknown)" in 352 let subject = Option.value (Types.Email.subject email) ~default:"(no subject)" in 353 354 let from_list = Option.value (Types.Email.from email) ~default:[] in 355 let from = match from_list with 356 | addr :: _ -> Types.Email_address.email addr 357 | [] -> "(unknown)" 358 in 359 360 let date = match Types.Email.received_at email with 361 | Some d -> String.sub (ISO8601.string_of_datetime (Unix.gmtime d)) 0 19 362 | None -> "(unknown)" 363 in 364 365 (* Get all keywords/flags *) 366 let keywords = match Types.Email.keywords email with 367 | Some kw -> kw 368 | None -> [] 369 in 370 371 (* Format keywords for display *) 372 let flag_strs = keywords |> List.map (fun kw -> 373 match kw with 374 | Types.Keywords.Draft -> "$draft" 375 | Types.Keywords.Seen -> "$seen" 376 | Types.Keywords.Flagged -> "$flagged" 377 | Types.Keywords.Answered -> "$answered" 378 | Types.Keywords.Forwarded -> "$forwarded" 379 | Types.Keywords.Phishing -> "$phishing" 380 | Types.Keywords.Junk -> "$junk" 381 | Types.Keywords.NotJunk -> "$notjunk" 382 | Types.Keywords.Custom c -> c 383 | Types.Keywords.Notify -> "$notify" 384 | Types.Keywords.Muted -> "$muted" 385 | Types.Keywords.Followed -> "$followed" 386 | Types.Keywords.Memo -> "$memo" 387 | Types.Keywords.HasMemo -> "$hasmemo" 388 | Types.Keywords.Autosent -> "$autosent" 389 | Types.Keywords.Unsubscribed -> "$unsubscribed" 390 | Types.Keywords.CanUnsubscribe -> "$canunsubscribe" 391 | Types.Keywords.Imported -> "$imported" 392 | Types.Keywords.IsTrusted -> "$istrusted" 393 | Types.Keywords.MaskedEmail -> "$maskedemail" 394 | Types.Keywords.New -> "$new" 395 | Types.Keywords.MailFlagBit0 -> "$MailFlagBit0" 396 | Types.Keywords.MailFlagBit1 -> "$MailFlagBit1" 397 | Types.Keywords.MailFlagBit2 -> "$MailFlagBit2" 398 ) in 399 400 Printf.printf "Email %d: %s\n" (i + 1) subject; 401 Printf.printf " ID: %s\n" id; 402 403 if verbose then begin 404 Printf.printf " From: %s\n" from; 405 Printf.printf " Date: %s\n" date; 406 end; 407 408 (* Show color if applicable *) 409 begin match flags_to_color keywords with 410 | Some color -> 411 let color_name = match color with 412 | `Red -> "Red" 413 | `Orange -> "Orange" 414 | `Yellow -> "Yellow" 415 | `Green -> "Green" 416 | `Blue -> "Blue" 417 | `Purple -> "Purple" 418 | `Gray -> "Gray" 419 in 420 Printf.printf " Color: %s\n" color_name 421 | None -> () 422 end; 423 424 Printf.printf " Flags: %s\n\n" 425 (if flag_strs = [] then "(none)" else String.concat ", " flag_strs) 426 ); 427 428 if List.length emails = 0 then 429 Printf.printf "No emails found matching criteria.\n" 430 431(* Command implementation *) 432let flag_command host user _password list add_flag remove_flag query from days 433 mailbox ids has_flag missing_flag limit dry_run color verbose : int = 434 (* Pack arguments into a record for easier passing *) 435 let _args : flag_manager_args = { 436 list; add_flag; remove_flag; query; from; days; mailbox; 437 ids; has_flag; missing_flag; limit; dry_run; color; verbose 438 } in 439 440 (* Main workflow would be implemented here using the JMAP library *) 441 Printf.printf "JMAP Flag Manager\n"; 442 Printf.printf "Server: %s\n" host; 443 Printf.printf "User: %s\n\n" user; 444 445 if list then 446 Printf.printf "Listing emails with their flags...\n\n" 447 else begin 448 if add_flag <> None then 449 Printf.printf "Adding flag: %s\n" (Option.get add_flag); 450 451 if remove_flag <> None then 452 Printf.printf "Removing flag: %s\n" (Option.get remove_flag); 453 454 if color <> None then 455 let color_name = match Option.get color with 456 | `Red -> "Red" 457 | `Orange -> "Orange" 458 | `Yellow -> "Yellow" 459 | `Green -> "Green" 460 | `Blue -> "Blue" 461 | `Purple -> "Purple" 462 | `Gray -> "Gray" 463 | `None -> "None" 464 in 465 Printf.printf "Setting color: %s\n" color_name; 466 end; 467 468 if query <> "" then 469 Printf.printf "Filtering by query: %s\n" query; 470 471 if from <> None then 472 Printf.printf "Filtering by sender: %s\n" (Option.get from); 473 474 if mailbox <> None then 475 Printf.printf "Filtering by mailbox: %s\n" (Option.get mailbox); 476 477 if ids <> [] then 478 Printf.printf "Operating on specific email IDs: %s\n" 479 (String.concat ", " ids); 480 481 if has_flag <> None then 482 Printf.printf "Filtering to emails with flag: %s\n" (Option.get has_flag); 483 484 if missing_flag <> None then 485 Printf.printf "Filtering to emails without flag: %s\n" (Option.get missing_flag); 486 487 Printf.printf "Limiting to %d emails\n" limit; 488 489 if dry_run then 490 Printf.printf "DRY RUN MODE - No changes will be made\n"; 491 492 Printf.printf "\n"; 493 494 (* This is where the actual JMAP calls would happen, like: 495 496 let manage_flags () = 497 let* (ctx, session) = Jmap.Unix.connect 498 ~host ~username:user ~password 499 ~auth_method:(Jmap.Unix.Basic(user, password)) () in 500 501 (* Get primary account ID *) 502 let account_id = match Jmap.get_primary_account session Jmap_email.capability_mail with 503 | Ok id -> id 504 | Error _ -> failwith "No mail account found" 505 in 506 507 (* Resolve mailbox name to ID if specified *) 508 let* mailbox_id_opt = match args.mailbox with 509 | None -> Lwt.return None 510 | Some name -> 511 (* This would use Mailbox/query and Mailbox/get to resolve the name *) 512 ... 513 in 514 515 (* Find emails to operate on *) 516 let* emails = 517 if args.ids <> [] then 518 (* Get emails by ID *) 519 let* result = Email.get ctx 520 ~account_id 521 ~ids:args.ids 522 ~properties:["id"; "subject"; "from"; "receivedAt"; "keywords"] in 523 524 match result with 525 | Error err -> 526 Printf.eprintf "Error: %s\n" (Jmap.Error.error_to_string err); 527 Lwt.return [] 528 | Ok (_, emails) -> Lwt.return emails 529 else 530 (* Find emails by query *) 531 let filter = build_filter account_id mailbox_id_opt args in 532 533 let* result = Email.query ctx 534 ~account_id 535 ~filter 536 ~sort:[Email_sort.received_newest_first ()] 537 ~limit:args.limit 538 ~properties:["id"] in 539 540 match result with 541 | Error err -> 542 Printf.eprintf "Error: %s\n" (Jmap.Error.error_to_string err); 543 Lwt.return [] 544 | Ok (ids, _) -> 545 (* Get full email objects for the matching IDs *) 546 let* result = Email.get ctx 547 ~account_id 548 ~ids 549 ~properties:["id"; "subject"; "from"; "receivedAt"; "keywords"] in 550 551 match result with 552 | Error err -> 553 Printf.eprintf "Error: %s\n" (Jmap.Error.error_to_string err); 554 Lwt.return [] 555 | Ok (_, emails) -> Lwt.return emails 556 in 557 558 (* Just list the emails with their flags *) 559 if args.list then 560 display_email_flags emails args.verbose; 561 Lwt.return_unit 562 else if List.length emails = 0 then 563 Printf.printf "No emails found matching criteria.\n"; 564 Lwt.return_unit 565 else 566 (* Perform flag operations *) 567 let ids = emails |> List.filter_map Types.Email.id in 568 569 if args.dry_run then 570 display_email_flags emails args.verbose; 571 Lwt.return_unit 572 else 573 (* Create patch object *) 574 let make_patch () = 575 let add_keywords = ref [] in 576 let remove_keywords = ref [] in 577 578 (* Handle add flag *) 579 Option.iter (fun flag -> 580 let keyword = flag_to_keyword flag in 581 add_keywords := keyword :: !add_keywords 582 ) args.add_flag; 583 584 (* Handle remove flag *) 585 Option.iter (fun flag -> 586 let keyword = flag_to_keyword flag in 587 remove_keywords := keyword :: !remove_keywords 588 ) args.remove_flag; 589 590 (* Handle color *) 591 Option.iter (fun color -> 592 (* First remove all color bits *) 593 remove_keywords := Types.Keywords.MailFlagBit0 :: !remove_keywords; 594 remove_keywords := Types.Keywords.MailFlagBit1 :: !remove_keywords; 595 remove_keywords := Types.Keywords.MailFlagBit2 :: !remove_keywords; 596 597 (* Then add the right combination for the requested color *) 598 if color <> `None then begin 599 let color_flags = color_to_flags color in 600 add_keywords := color_flags @ !add_keywords 601 end 602 ) args.color; 603 604 Email.make_patch 605 ~add_keywords:!add_keywords 606 ~remove_keywords:!remove_keywords 607 () 608 in 609 610 let patch = make_patch () in 611 612 let* result = Email.update ctx 613 ~account_id 614 ~ids 615 ~update_each:(fun _ -> patch) in 616 617 match result with 618 | Error err -> 619 Printf.eprintf "Error: %s\n" (Jmap.Error.error_to_string err); 620 Lwt.return_unit 621 | Ok updated -> 622 Printf.printf "Successfully updated %d emails.\n" (List.length updated); 623 Lwt.return_unit 624 *) 625 626 if list then begin 627 (* Simulate having found a few emails *) 628 let count = 3 in 629 Printf.printf "Found %d matching emails:\n\n" count; 630 Printf.printf "Email 1: Meeting Agenda\n"; 631 Printf.printf " ID: email123\n"; 632 if verbose then begin 633 Printf.printf " From: alice@example.com\n"; 634 Printf.printf " Date: 2023-04-15 09:30:00\n"; 635 end; 636 Printf.printf " Flags: $seen, $flagged, $answered\n\n"; 637 638 Printf.printf "Email 2: Project Update\n"; 639 Printf.printf " ID: email124\n"; 640 if verbose then begin 641 Printf.printf " From: bob@example.com\n"; 642 Printf.printf " Date: 2023-04-16 14:45:00\n"; 643 end; 644 Printf.printf " Color: Red\n"; 645 Printf.printf " Flags: $seen, $MailFlagBit0\n\n"; 646 647 Printf.printf "Email 3: Weekly Newsletter\n"; 648 Printf.printf " ID: email125\n"; 649 if verbose then begin 650 Printf.printf " From: newsletter@example.com\n"; 651 Printf.printf " Date: 2023-04-17 08:15:00\n"; 652 end; 653 Printf.printf " Flags: $seen, $notjunk\n\n"; 654 end else if add_flag <> None || remove_flag <> None || color <> None then begin 655 Printf.printf "Would modify %d emails:\n" 2; 656 if dry_run then 657 Printf.printf "(Dry run mode - no changes made)\n\n" 658 else 659 Printf.printf "Changes applied successfully\n\n"; 660 end; 661 662 (* List standard flags if no other actions specified *) 663 if not list && add_flag = None && remove_flag = None && color = None then begin 664 Printf.printf "Standard flags:\n"; 665 get_standard_flags() |> List.iter (fun (flag, desc) -> 666 Printf.printf " $%-12s %s\n" flag desc 667 ); 668 669 Printf.printf "\nColor flags:\n"; 670 Printf.printf " $MailFlagBit0 Red\n"; 671 Printf.printf " $MailFlagBit1 Orange\n"; 672 Printf.printf " $MailFlagBit2 Yellow\n"; 673 Printf.printf " $MailFlagBit0+1 Green\n"; 674 Printf.printf " $MailFlagBit0+2 Blue\n"; 675 Printf.printf " $MailFlagBit1+2 Purple\n"; 676 Printf.printf " $MailFlagBit0+1+2 Gray\n"; 677 end; 678 679 (* Since we're only type checking, we'll exit with success *) 680 0 681 682(* Command definition *) 683let flag_cmd = 684 let doc = "manage email flags using JMAP" in 685 let man = [ 686 `S Manpage.s_description; 687 `P "Lists, adds, and removes flags (keywords) from emails using JMAP."; 688 `P "Demonstrates JMAP's flag/keyword management capabilities."; 689 `S Manpage.s_examples; 690 `P " $(mname) -h jmap.example.com -u user@example.com -p secret123 --list"; 691 `P " $(mname) -h jmap.example.com -u user@example.com -p secret123 --add flagged --from boss@example.com"; 692 `P " $(mname) -h jmap.example.com -u user@example.com -p secret123 --color red --mailbox Inbox --has-flag seen --missing-flag flagged"; 693 ] in 694 695 let cmd = 696 Cmd.v 697 (Cmd.info "jmap-flag-manager" ~version:"1.0" ~doc ~man) 698 Term.(const flag_command $ host_arg $ user_arg $ password_arg $ 699 list_arg $ add_flag_arg $ remove_flag_arg $ query_arg $ 700 from_arg $ days_arg $ mailbox_arg $ ids_arg $ has_flag_arg $ 701 missing_flag_arg $ limit_arg $ dry_run_arg $ color_arg $ verbose_arg) 702 in 703 cmd 704 705(* Main entry point *) 706let () = exit (Cmd.eval' flag_cmd)