Created
February 15, 2023 15:50
-
-
Save extratone/a04208125528bef6ace590665500b6b7 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| (* | |
| Remove Duplicate Messages | |
| copyright Jolly Roger <jollyroger@pobox.com> | |
| http://jollyroger.kicks-ass.org/software/ | |
| *) | |
| property pDebug : false | |
| property pLogging : true -- generate a log file? | |
| property pShowLogInTerminal : true -- show log in terminal window? | |
| property pAlertTimeout : 15 -- number of seconds to give up on alerts after no response from user | |
| property pDialogTimeout : 30 -- number of seconds to give up on interactive dialogs after no response from user | |
| property pScriptname : "Remove Duplicate Messages" | |
| property pBaseFoldername : pScriptname | |
| property pLogFileName : (pScriptname & ".log" as text) | |
| property pArchiveFoldername : "Archived Duplicates" | |
| global removeDuplicates -- remove duplicate messages? specified at runtime by user (values: true, false, or "ask") | |
| global archiveDuplicates -- archive duplicate messages? specified at runtime by user (values: true, false) | |
| global baseFolder -- computed at runtime with desktop path and pBaseFoldername | |
| global logFolder -- computed at run time with base folder path | |
| global archiveFolder -- computed at run time with base folder path and pArchiveFoldername | |
| global enabledAccounts -- list of all enabled Mail accounts whose "include when automatically checking for new messages" property is enabled | |
| global selectedMessages -- a list of selected messages in Mail | |
| global encounteredMessages -- a list of encountered messages | |
| global archivedMessages -- a list of archived messages | |
| global removedMessages -- a list of removed message IDs | |
| global duplicateMessagesCount -- duplicate message counter | |
| global removedMessagesCount -- removed message counter | |
| global archivedMessagesCount -- archived message counter | |
| global automaticCheckingDisabled | |
| global duplicatesWereShown | |
| global duplicatesAreShown | |
| on run | |
| -- clear message lists and counters | |
| set enabledAccounts to {} | |
| set selectedMessages to {} | |
| set encounteredMessages to {} | |
| set archivedMessages to {} | |
| set removedMessages to {} | |
| set duplicateMessagesCount to 0 | |
| set removedMessagesCount to 0 | |
| set archivedMessagesCount to 0 | |
| set automaticCheckingDisabled to false | |
| set duplicatesAreShown to false | |
| -- get desktop folder | |
| set desktopFolder to the path to the desktop from the user domain as text -- path to alias already has ending : path delimiter | |
| -- set base folder within desktop folder | |
| set baseFolder to (desktopFolder & pBaseFoldername & ":" as text) -- have to add : path delimiter manually to textual path | |
| -- set log folder to base folder | |
| set logFolder to baseFolder | |
| -- set archive folder within base folder | |
| set archiveFolder to (baseFolder & pArchiveFoldername & ":" as text) -- have to add : path delimiter manually to textual path | |
| -- create base folder, if needed | |
| set baseFolder to my CreateFolder(desktopFolder, pBaseFoldername) | |
| -- create archive folder, if needed | |
| set archiveFolder to my CreateFolder(baseFolder, pArchiveFoldername) | |
| -- start up | |
| my LogEntry("Starting up...") | |
| my LogEntry("Base folder: " & POSIX path of baseFolder) | |
| my LogEntry("Archive folder: " & POSIX path of archiveFolder) | |
| -- display informational dialog about this script | |
| my DisplayAbout() | |
| -- view log in a terminal window | |
| if pLogging and pShowLogInTerminal then ViewLog() | |
| -- get selected messages from Mail | |
| my LogEntry("Getting selected messages from Mail...") | |
| set gotSelectedMessages to false | |
| try | |
| tell application "Mail" to set selectedMessages to the selection | |
| set selectedMessageCount to count of selectedMessages | |
| if selectedMessageCount is 0 then | |
| set selectedMessageCountPhrase to "No messages are selected." | |
| else if selectedMessageCount < 2 then | |
| set selectedMessageCountPhrase to selectedMessageCount & " message is selected." | |
| else | |
| set selectedMessageCountPhrase to selectedMessageCount & " messages are selected." | |
| end if | |
| my LogEntry(selectedMessageCountPhrase) | |
| if selectedMessageCount < 2 then | |
| -- not enough messages are selected | |
| tell application "System Events" | |
| activate | |
| display alert "Select Messages First" message (selectedMessageCountPhrase & " The script will now abort." & return & return & "If you want to select all messages, choose Edit > Select All from the Mail menu bar before running this script." as text) as critical buttons {"Quit"} default button "Quit" | |
| end tell | |
| else | |
| -- enough messages are selected | |
| set gotSelectedMessages to true | |
| end if | |
| on error errorMessage number errorNumber | |
| -- could not obtain selected message list | |
| my LogEntry("Could not get selected messages. Error Message: \"" & errorMessage & "\" (" & errorNumber & ")") | |
| tell application "System Events" | |
| activate | |
| display alert "Could Not Get Selected Messages" message ("The script could not obtain a list of selected messages from Apple Mail." & return & return & "Error Message: \"" & errorMessage & "\" (" & errorNumber & ")" as text) as critical buttons {"Quit"} default button "Quit" | |
| end tell | |
| end try | |
| if gotSelectedMessages then | |
| my LogEntry("Processing " & selectedMessageCount & " messages.") | |
| -- ask user whether to remove duplicate messages | |
| set removeDuplicates to my GetUserRemoveDuplicateMessagesChoice() | |
| -- ask user whether to archive duplicate messages before removal | |
| set archiveDuplicates to my GetUserArchiveDuplicateMessagesChoice() | |
| -- initialize hash table with size based on number of messages | |
| my LogEntry("Preparing encountered message and archived message hash tables...") | |
| -- encountered messages | |
| if (selectedMessageCount * 2 > 1000) then | |
| set hashTable's initSize to (selectedMessageCount * 2) as integer | |
| end if | |
| set encounteredMessages to hashTable's newInstance()'s init() | |
| -- archived messages | |
| set hashTable's initSize to 1000 as integer | |
| set archivedMessages to hashTable's newInstance()'s init() | |
| my LogEntry("Hash tables tables are initialized.") | |
| -- see if mail is showing duplicate messages | |
| set duplicatesWereShown to my GetMailShowDuplicatesSetting() | |
| -- hide duplicate messages, if needed | |
| if not duplicatesWereShown then | |
| my LogEntry("Temporarily configuring Mail to show duplicate messages.") | |
| set duplicatesAreShown to SetMailShowDuplicatesSetting(true) | |
| else | |
| my LogEntry("Duplicate messages are already shown in Mail.") | |
| end if | |
| -- disable automatic checking for all Mail accounts that have it enabled | |
| try | |
| tell application "Mail" to set enabledAccounts to every account whose enabled is true and include when getting new mail is true | |
| if (count of enabledAccounts) is greater than 0 then | |
| -- build a display list of enabled Mail accounts with automatic checking enabled | |
| set accountDisplayList to "" | |
| repeat with nextAccount in enabledAccounts | |
| tell application "Mail" to set accountName to the name of nextAccount | |
| set accountDisplayList to accountDisplayList & (accountName & ", ") as text | |
| end repeat | |
| set accountDisplayList to text 1 through ((count of accountDisplayList) - 2) of accountDisplayList -- remove trailing comma and space | |
| -- inform user that mail checking will be disabled until script finishes | |
| set alertMsg to "Mail is configured to automatically check for new messages for these accounts: " & accountDisplayList & return & return & "Certain Mail operations (such as importing mailboxes, and asking if attachments should be downloaded when new messages arrive) may cause Mail to stop responding to script commands while processing large numbers of messages. " & return & return & "In order to minimize such potential interruptions, this property is being temporarily disabled for these accounts, and will be re-enabled once the script is finished processing messages." | |
| tell application "System Events" | |
| activate | |
| display alert "Disabling Automatic Mail Checking" message alertMsg as informational buttons {"Continue"} default button "Continue" giving up after pAlertTimeout | |
| end tell | |
| -- disable automatic checking for enabled Mail accounts | |
| my LogEntry("Disabling automatic mail checking for these Mail accounts: " & accountDisplayList) | |
| tell application "Mail" to set include when getting new mail of every account to false | |
| set automaticCheckingDisabled to true | |
| end if | |
| on error errorMessage number errorNumber | |
| -- NOTE: Some Catalina (Mail) versions have a bug that causes Mail to generate a "AppleEvent handler failed (-10000)" error in response to scripts asking for a list of all enabled accounts | |
| -- inform user that accounts will not be disabled during script operation | |
| set alertMsg to "When Mail is configured to automatically check for new messages, certain Mail operations (such as mail rule processing, importing mailboxes, and asking if attachments should be downloaded when new messages arrive) may cause Mail to stop responding to script commands while processing large numbers of messages. " & return & return & "Due to a bug in this version of macOS, this script cannot ask Mail to temporarily disable this property while the script runs. Here is the error message generated by Mail: " & return & return & errorMessage & "(" & errorNumber & ")" | |
| tell application "System Events" | |
| activate | |
| display alert "Cannot Disable Automatic Mail Checking" message alertMsg as informational buttons {"Understood"} default button "Understood" | |
| end tell | |
| end try | |
| -- start timer | |
| set startTime to the current date | |
| -- process selected messages | |
| repeat with m from 1 to the selectedMessageCount | |
| -- get a reference to the next message in the list | |
| set nextMessage to (a reference to selectedMessages's item m) | |
| -- process it | |
| set userRequestedAbort to my ProcessMessage(nextMessage, m, selectedMessageCount) | |
| -- abort processing if requested | |
| if userRequestedAbort then | |
| my LogEntry("Aborting processing as requested by user after message " & m & " of " & selectedMessageCount) | |
| exit repeat | |
| end if | |
| end repeat | |
| -- output hash table to a text file for debugging purposes | |
| if pDebug then | |
| encounteredMessages's debug() | |
| end if | |
| -- end timer and report elapsed time | |
| set endTime to the current date | |
| my LogEntry("Time elapsed: " & endTime - startTime & " seconds.") | |
| -- re-enable automatic checking for accounts that previously had it enabled | |
| if automaticCheckingDisabled then | |
| set displayMessage to "Re-enabling automatic checking for these Mail accounts: " & accountDisplayList | |
| my LogEntry(displayMessage) | |
| repeat with nextAccount in enabledAccounts | |
| tell application "Mail" to set include when getting new mail of nextAccount to true | |
| end repeat | |
| end if | |
| -- hide duplicate messages if they were previously hidden | |
| if not duplicatesWereShown then | |
| my LogEntry("Re-configuring Mail to hide duplicate messages (the default factory configuration).") | |
| set duplicatesAreShown to SetMailShowDuplicatesSetting(false) | |
| end if | |
| -- report duplicate messages found, removed, and archived | |
| set alertTitle to "Final Report" | |
| if duplicateMessagesCount is greater than 0 then | |
| set alertMsg to (duplicateMessagesCount & " duplicate messages were found. " as string) | |
| my LogEntry(duplicateMessagesCount & " duplicate messages were found.") | |
| if removedMessagesCount is greater than 0 then | |
| set alertMsg to alertMsg & (removedMessagesCount & " duplicate messages were removed. " as string) | |
| my LogEntry(removedMessagesCount & " duplicate messages were removed.") | |
| if (archiveDuplicates is true) then | |
| set alertMsg to alertMsg & ("All duplicate messages that were removed are now archived here:" & return & return & archiveFolder's POSIX path as string) | |
| my LogEntry("All removed duplicate messages are now archived here: " & archiveFolder's POSIX path) | |
| else | |
| set alertMsg to alertMsg & ("No removed duplicate messages were archived.") | |
| my LogEntry("No removed duplicate messages were archived.") | |
| end if | |
| else | |
| set alertMsg to alertMsg & ("No duplicate messages were removed. " as string) | |
| my LogEntry("No duplicate messages were removed.") | |
| end if | |
| else | |
| set alertMsg to "No duplicate messages were found or removed." | |
| my LogEntry("No duplicate messages were found or removed.") | |
| end if | |
| tell application "System Events" | |
| activate | |
| display alert alertTitle message alertMsg as informational buttons {"Continue"} default button 1 giving up after pAlertTimeout | |
| end tell | |
| else | |
| my LogEntry("No messages were selected, or an error occurred. Aborting script.") | |
| end if | |
| -- deselect currently selected messages so that deleted messages are no longer selected | |
| (* | |
| note: when messages are "deleted" they are moved to the trash mailbox, but are not removed from the current selection (highlighted messages). therefore subsequent runs of the script with previously deleted messages still in the selection can cause the script to find those previously deleted messages and report them as false positives to the user. clearing the selection fixes this issue. | |
| *) | |
| tell application "Mail" to set message viewer 1's selected messages to {} | |
| -- bail out | |
| my LogEntry("Finished. Goodbye.") | |
| end run | |
| -------------------------------------------------------------------------------------------- | |
| on ProcessMessage(selectedMessage, selectedIndex, selectedCount) | |
| set userRequestedAbort to false | |
| set keeper to missing value | |
| set keeperID to missing value | |
| set keeperMID to missing value | |
| set keeperSubject to missing value | |
| set keeperSender to missing value | |
| set keeperSentDate to missing value | |
| set keeperMailbox to missing value | |
| set keeperSource to missing value | |
| set keeperAccount to missing value | |
| set keeperAttachmentNames to missing value | |
| set keeperAttachmentTotalSize to missing value | |
| set clunker to missing value | |
| set clunkerID to missing value | |
| set clunkerMID to missing value | |
| set clunkerSubject to missing value | |
| set clunkerSender to missing value | |
| set clunkerSentDate to missing value | |
| set clunkerMailbox to missing value | |
| set clunkerSource to missing value | |
| set clunkerAccount to missing value | |
| set clunkerAttachmentNames to missing value | |
| set clunkerAttachmentTotalSize to missing value | |
| -- update AppleScript progress | |
| tell application "Mail" to my LogEntry("Examining " & selectedIndex & " of " & selectedCount & ": [" & the id of the selectedMessage & "] \"" & the subject of the selectedMessage & "\".") | |
| -- get selected message details | |
| set selectedMessageDetails to my GetMessageDetails(selectedMessage) | |
| set selectedID to the appleid of selectedMessageDetails | |
| set selectedMID to the messageid of selectedMessageDetails | |
| set selectedSubject to the subject of selectedMessageDetails | |
| set selectedSender to the sender of selectedMessageDetails | |
| set selectedSentDate to the datesent of selectedMessageDetails | |
| set selectedMailbox to the mailbox of selectedMessageDetails | |
| set selectedAccount to the account of selectedMessageDetails | |
| set selectedSource to the source of selectedMessageDetails | |
| set selectedAttachmentNames to the attachmentnames of selectedMessageDetails | |
| set selectedAttachmentCount to the attachmentcount of the selectedMessageDetails | |
| set selectedAttachmentTotalSize to the attachmentsize of the selectedMessageDetails | |
| if pDebug then | |
| my LogEntry(" Selected Message " & selectedID & ":") | |
| my LogEntry(" Message-ID: " & selectedMID) | |
| my LogEntry(" Mailbox: " & selectedMailbox & " (Account: " & selectedAccount & ")") | |
| my LogEntry(" Subject: " & selectedSubject) | |
| my LogEntry(" From: " & selectedSender) | |
| my LogEntry(" Sent: " & selectedSentDate) | |
| if selectedAttachmentCount = 0 then | |
| my LogEntry(" Attachments: none") | |
| else | |
| my LogEntry(" Attachments: (" & selectedAttachmentCount & ") " & selectedAttachmentNames & " (" & selectedAttachmentTotalSize & " bytes)") | |
| end if | |
| end if | |
| -- create hash string for selected message | |
| set hashString to (selectedSubject & " " & selectedSender & " " & FormatDateTime(selectedSentDate)) as text | |
| if pDebug then my LogEntry(" DEBUG: selected message hash: " & hashString) | |
| -- see if we have encountered this message before | |
| set potentialMatches to {} | |
| if encounteredMessages's keyExists(hashString) then | |
| if pDebug then my LogEntry(" DEBUG: we've encountered a message with this hash before") | |
| set potentialMatches to encounteredMessages's valueForKey(hashString) | |
| end if | |
| if pDebug then my LogEntry(" DEBUG: potential duplicates: " & the (count of potentialMatches)) | |
| -- review potential duplicate messages | |
| set numPotentialMatches to the count of potentialMatches | |
| if numPotentialMatches > 0 then | |
| -- compare selected message with each matching encountered message | |
| if pDebug then LogEntry(" DEBUG: comparing with " & the numPotentialMatches & " matching encountered messages") | |
| repeat with nextPotential in potentialMatches | |
| -- get potential matching message details | |
| set nextMessage to nextPotential's object | |
| set nextMessageDetails to my GetMessageDetails(nextMessage) | |
| set nextID to the appleid of nextMessageDetails | |
| set nextMID to the messageid of nextMessageDetails | |
| set nextSubject to the subject of nextMessageDetails | |
| set nextSender to the sender of nextMessageDetails | |
| set nextSentDate to the datesent of nextMessageDetails | |
| set nextSource to the source of nextMessageDetails | |
| set nextMailbox to the mailbox of nextMessageDetails | |
| set nextAccount to the account of nextMessageDetails | |
| set nextAttachmentNames to the attachmentnames of nextMessageDetails | |
| set nextAttachmentCount to the attachmentcount of the nextMessageDetails | |
| set nextAttachmentTotalSize to the attachmentsize of the nextMessageDetails | |
| if pDebug then | |
| my LogEntry(" Next Message " & nextID & ":") | |
| my LogEntry(" Message-ID: " & nextMID) | |
| my LogEntry(" Mailbox: " & nextMailbox & " (Account: " & nextAccount & ")") | |
| my LogEntry(" Sent: " & nextSentDate) | |
| my LogEntry(" From: " & nextSender) | |
| my LogEntry(" Subject: " & nextSubject) | |
| if nextAttachmentCount = 0 then | |
| my LogEntry(" Attachments: none") | |
| else | |
| my LogEntry(" Attachments: (" & nextAttachmentCount & ") " & nextAttachmentNames & " (" & nextAttachmentTotalSize & " bytes)") | |
| end if | |
| end if | |
| -- ignore previously encountered messages that have been deleted | |
| if nextID is not in removedMessages then | |
| -- compare selected message with this potential duplicate message | |
| if (selectedMID is "") or (selectedMID is nextMID) then | |
| -- message ID is empty or matches | |
| my LogEntry(" This is a duplicate message (Message-ID header is empty or matches previously encountered message " & nextID & ").") | |
| -- increment counter | |
| set duplicateMessagesCount to duplicateMessagesCount + 1 | |
| -- compare attachment sizes | |
| if selectedAttachmentTotalSize > nextAttachmentTotalSize then | |
| -- selected message has a larger attachment size | |
| -- keep selected message and clunk matching encountered message | |
| my LogEntry(" This message has a larger attachment size than matching message " & nextID & " (" & selectedAttachmentTotalSize & " bytes > " & nextAttachmentTotalSize & " bytes).") | |
| my LogEntry(" Keeping this message and removing message " & nextID & " instead.") | |
| set keeper to selectedMessage | |
| set keeperID to selectedID | |
| set keeperMID to selectedMID | |
| set keeperSubject to selectedSubject | |
| set keeperSender to selectedSender | |
| set keeperSentDate to selectedSentDate | |
| set keeperMailbox to selectedMailbox | |
| set keeperAccount to selectedAccount | |
| set keeperSource to selectedSource | |
| set keeperAttachmentNames to selectedAttachmentNames | |
| set keeperAttachmentCount to selectedAttachmentCount | |
| set keeperAttachmentTotalSize to selectedAttachmentTotalSize | |
| set clunker to nextMessage | |
| set clunkerID to nextID | |
| set clunkerMID to nextMID | |
| set clunkerSubject to nextSubject | |
| set clunkerSender to nextSender | |
| set clunkerSentDate to nextSentDate | |
| set clunkerMailbox to nextMailbox | |
| set clunkerAccount to nextAccount | |
| set clunkerSource to nextSource | |
| set clunkerAttachmentNames to nextAttachmentNames | |
| set clunkerAttachmentCount to nextAttachmentCount | |
| set clunkerAttachmentTotalSize to nextAttachmentTotalSize | |
| -- remember selected message as previously encountered | |
| LogEntry(" Adding this message to encountered messages list") | |
| encounteredMessages's setValueforKey(hashString, {{object:selectedMessage, appleid:selectedID, messageid:selectedMID, subject:selectedSubject, sender:selectedSender, sentdate:selectedSentDate, mailbox:selectedMailbox, account:selectedAccount}}) | |
| else | |
| -- encountered message has an equal or larger attachment size - keep it | |
| -- keep previously encountered message | |
| (* | |
| my LogEntry(" Previously encountered message " & nextID & " has an equal or larger attachment size than this message (" & nextAttachmentTotalSize & " bytes > " & selectedAttachmentTotalSize & " bytes).") | |
| my LogEntry(" Removing this duplicate message.") | |
| *) | |
| set keeper to nextMessage | |
| set keeperID to nextID | |
| set keeperMID to nextMID | |
| set keeperSubject to nextSubject | |
| set keeperSender to nextSender | |
| set keeperSentDate to nextSentDate | |
| set keeperMailbox to nextMailbox | |
| set keeperAccount to nextAccount | |
| set keeperSource to nextSource | |
| set keeperAttachmentNames to nextAttachmentNames | |
| set keeperAttachmentCount to nextAttachmentCount | |
| set keeperAttachmentTotalSize to nextAttachmentTotalSize | |
| set clunker to selectedMessage | |
| set clunkerID to selectedID | |
| set clunkerMID to selectedMID | |
| set clunkerSubject to selectedSubject | |
| set clunkerSender to selectedSender | |
| set clunkerSentDate to selectedSentDate | |
| set clunkerMailbox to selectedMailbox | |
| set clunkerAccount to selectedAccount | |
| set clunkerSource to selectedSource | |
| set clunkerAttachmentNames to selectedAttachmentNames | |
| set clunkerAttachmentCount to selectedAttachmentCount | |
| set clunkerAttachmentTotalSize to selectedAttachmentTotalSize | |
| end if | |
| my LogEntry(" Keeping Message " & keeperID & ":") | |
| my LogEntry(" Message-ID: " & keeperMID) | |
| my LogEntry(" Mailbox: " & keeperMailbox & " (Account: " & keeperAccount & ")") | |
| my LogEntry(" Subject: " & keeperSubject) | |
| my LogEntry(" From: " & keeperSender) | |
| my LogEntry(" Sent: " & keeperSentDate) | |
| if keeperAttachmentCount = 0 then | |
| my LogEntry(" Attachments: none") | |
| else | |
| my LogEntry(" Attachments: (" & keeperAttachmentCount & ") " & keeperAttachmentNames & " (" & keeperAttachmentTotalSize & " bytes)") | |
| end if | |
| my LogEntry(" DELETING Message " & clunkerID & ":") | |
| my LogEntry(" Message-ID: " & clunkerMID) | |
| my LogEntry(" Mailbox: " & clunkerMailbox & " (Account: " & clunkerAccount & ")") | |
| my LogEntry(" Subject: " & clunkerSubject) | |
| my LogEntry(" From: " & clunkerSender) | |
| my LogEntry(" Sent: " & clunkerSentDate) | |
| if clunkerAttachmentCount = 0 then | |
| my LogEntry(" Attachments: none") | |
| else | |
| my LogEntry(" Attachments: (" & clunkerAttachmentCount & ") " & clunkerAttachmentNames & " (" & clunkerAttachmentTotalSize & " bytes)") | |
| end if | |
| -- archive the clunker message | |
| if (archiveDuplicates) then | |
| my SaveMessage(clunkerID, clunkerSubject, clunkerSource) | |
| set archivedMessagesCount to archivedMessagesCount + 1 | |
| end if | |
| -- determine whether we should remove the duplicate message | |
| set shouldRemove to false | |
| if (removeDuplicates is "ask") then | |
| -- user specified that we should ask for each duplicate message - allow user to choose now | |
| if keeperAttachmentCount = 0 then | |
| set keeperAttachmentPhrase to "none" | |
| else | |
| set keeperAttachmentPhrase to "(" & keeperAttachmentCount & ") " & keeperAttachmentNames & " (" & keeperAttachmentTotalSize & " bytes)" | |
| end if | |
| if clunkerAttachmentCount = 0 then | |
| set clunkerAttachmentPhrase to "none" | |
| else | |
| set clunkerAttachmentPhrase to "(" & clunkerAttachmentCount & ") " & clunkerAttachmentNames & " (" & clunkerAttachmentTotalSize & " bytes)" | |
| end if | |
| set alertMessage to "Message " & clunkerID & " matches previously encountered message:" & return & return & ¬ | |
| "Keeping Message " & keeperID & ":" & return & ¬ | |
| "• Message-ID: " & keeperMID & return & ¬ | |
| "• Mailbox: " & keeperMailbox & " (Account: " & keeperAccount & ")" & return & ¬ | |
| "• Subject: " & keeperSubject & return & ¬ | |
| "• From: " & keeperSender & return & ¬ | |
| "• Sent: " & keeperSentDate & return & ¬ | |
| "• Attachments: " & keeperAttachmentPhrase & return & return & ¬ | |
| "REMOVING Message " & clunkerID & ":" & return & ¬ | |
| "• Message-ID: " & clunkerMID & return & ¬ | |
| "• Mailbox: " & clunkerMailbox & " (Account: " & clunkerAccount & ")" & return & ¬ | |
| "• Subject: " & clunkerSubject & return & ¬ | |
| "• From: " & clunkerSender & return & ¬ | |
| "• Sent: " & clunkerSentDate & return & ¬ | |
| "• Attachments: " & clunkerAttachmentPhrase & return & return & ¬ | |
| "Would you like to remove this duplicate message now?" | |
| tell application "System Events" | |
| activate | |
| set dResult to display alert "Duplicate Message Encountered" message alertMessage as critical buttons {"Quit", "Remove", "Leave In Place"} default button "Leave In Place" giving up after pDialogTimeout | |
| end tell | |
| set userChoice to dResult's button returned | |
| set userRequestedAbort to (userChoice is "Quit") | |
| set shouldRemove to (userChoice is "Remove") | |
| if userRequestedAbort then | |
| my LogEntry(" User interactively chose to quit processing messages.") | |
| else | |
| if shouldRemove then | |
| my LogEntry(" User interactively chose to remove duplicate message " & clunkerID & ".") | |
| else | |
| my LogEntry(" User interactively chose to retain duplicate message " & clunkerID & ".") | |
| end if | |
| end if | |
| else | |
| -- user has already specified whether to remove duplicates | |
| set shouldRemove to removeDuplicates | |
| end if | |
| -- remove duplicate message | |
| if (shouldRemove) then | |
| if (not pDebug) then | |
| -- remove | |
| my LogEntry("Removing duplicate message " & clunkerID & ".") | |
| set messageMovedToTrash to false | |
| try | |
| -- this generates errors when account is "On My Mac" | |
| tell application "Mail" to move clunker to mailbox "Trash" of account clunkerAccount | |
| my LogEntry("Message was removed.") | |
| set messageMovedToTrash to true | |
| on error errorMessage number errorNumber | |
| my LogEntry("ERROR: Could not remove duplicate message " & clunkerID & " due to an error: " & errorMessage & " (" & errorNumber & "). I will try an alternative method.") | |
| end try | |
| if not messageMovedToTrash then | |
| try | |
| tell application "Mail" to move clunker to mailbox "Deleted Messages" | |
| my LogEntry("Message was removed.") | |
| set messageMovedToTrash to true | |
| on error errorMessage number errorNumber | |
| my LogEntry("ERROR: Could not remove duplicate message " & clunkerID & " due to an error: " & errorMessage & " (" & errorNumber & "). Giving up.") | |
| end try | |
| end if | |
| -- keep track of removed messages | |
| if messageMovedToTrash then | |
| set the end of removedMessages to clunkerID | |
| set removedMessagesCount to removedMessagesCount + 1 | |
| end if | |
| else | |
| my LogEntry(" NOTICE: Since pDebug property is set to true, I will NOT remove duplicate message " & clunkerID & ".") | |
| end if | |
| else | |
| my LogEntry(" NOTICE: Since you specified that you want duplicate messages left in place, the script will NOT remove duplicate message " & clunkerID & ".") | |
| end if | |
| -- since we've found a match, we don't need to look any further | |
| exit repeat | |
| else | |
| -- remember selected message as previously encountered | |
| LogEntry(" Adding this message to encountered messages list") | |
| encounteredMessages's setValueforKey(hashString, {{object:selectedMessage, appleid:selectedID, messageid:selectedMID, subject:selectedSubject, sender:selectedSender, sentdate:selectedSentDate, mailbox:selectedMailbox, account:selectedAccount}}) | |
| end if | |
| end if | |
| end repeat | |
| else | |
| -- no matching messages found | |
| if pDebug then LogEntry("DEBUG: no potential duplicates found in encountered message list") | |
| -- store this message in the encountered messages list or hash table | |
| LogEntry(" Adding this message to encountered messages list") | |
| encounteredMessages's setValueforKey(hashString, {{object:selectedMessage, appleid:selectedID, messageid:selectedMID, subject:selectedSubject, sender:selectedSender, sentdate:selectedSentDate, mailbox:selectedMailbox, account:selectedAccount}}) | |
| end if | |
| -- return true if user wants to quit processing | |
| return userRequestedAbort | |
| end ProcessMessage | |
| -------------------------------------------------------------------------------------------- | |
| on GetMessageDetails(thisMessage) | |
| tell application "Mail" | |
| set thisMessageID to thisMessage's id | |
| set thisMessageMID to thisMessage's message id | |
| set thisMessageSubject to thisMessage's subject | |
| set thisMessageSender to thisMessage's sender | |
| set thisMessageSentDate to thisMessage's date sent | |
| set thisMessageMailbox to the name of thisMessage's mailbox | |
| try | |
| set thisMessageAccount to the name of the account of (thisMessage's mailbox) | |
| on error | |
| set thisMessageAccount to "On My Mac" -- "On My Mac" messages have no associated account | |
| end try | |
| set thisMessageSource to thisMessage's source as rich text | |
| end tell | |
| try | |
| tell application "Mail" to set thisMessageAttachmentNames to my ListToString(the name of every mail attachment of thisMessage) | |
| on error errMessage number errNumber | |
| set thisMessageAttachmentNames to "" | |
| if pDebug then my LogEntry("DEBUG: ERROR: Could not get attachment names for next message: [" & thisMessageID & "] \"" & thisMessageSubject & "\" due to error: " & errMessage & " (" & errNumber & ")") | |
| end try | |
| try | |
| tell application "Mail" to set thisMessageAttachmentCount to the count of the mail attachments of thisMessage | |
| on error errMessage number errNumber | |
| set thisMessageAttachmentCount to 0 | |
| if pDebug then my LogEntry("DEBUG: ERROR: Could not get attachment count for next message: [" & thisMessageID & "] \"" & thisMessageSubject & "\" due to error: " & errMessage & " (" & errNumber & ")") | |
| end try | |
| try | |
| tell application "Mail" to set thisMessageAttachmentTotalSize to my SumListOfNumbers(the file size of every mail attachment of thisMessage) | |
| on error errMessage number errNumber | |
| set thisMessageAttachmentTotalSize to 0 | |
| if pDebug then my LogEntry("DEBUG: ERROR: Could not get attachment sizes for next message: [" & thisMessageID & "] \"" & thisMessageSubject & "\" due to error: " & errMessage & " (" & errNumber & ")") | |
| end try | |
| (* return details in the format of an associative labeled array: | |
| { | |
| id, | |
| messageid, | |
| subject, | |
| sender, | |
| datesent, | |
| mailbox, | |
| account, | |
| source, | |
| attachmentnames, | |
| attachmentcount, | |
| attachmentsize | |
| } | |
| *) | |
| return {appleid:thisMessageID, messageid:thisMessageMID, subject:thisMessageSubject, sender:thisMessageSender, datesent:thisMessageSentDate, mailbox:thisMessageMailbox, account:thisMessageAccount, source:thisMessageSource, attachmentnames:thisMessageAttachmentNames, attachmentcount:thisMessageAttachmentCount, attachmentsize:thisMessageAttachmentTotalSize} | |
| end GetMessageDetails | |
| -------------------------------------------------------------------------------------------- | |
| on SaveMessage(msgID, msgSubject, msgSource) | |
| -- if subject is empty, make it "Untitled" | |
| if msgSubject is "" then set msgSubject to "Untitled" | |
| -- calculate archived message filename | |
| set outFilename to my TrimStringToLength(msgSubject, 255 - 4) -- max filename length is 255 characters minus 4 characters for filename extension | |
| set outFilename to my FixFilename(outFilename & ".eml") | |
| set outFilename to my MakeFilenameUnique(archiveFolder, outFilename) | |
| set outFile to (archiveFolder & outFilename) as text | |
| my LogEntry("Archiving message " & msgID & " in file: " & outFilename) | |
| -- write the file | |
| set fileID to open for access file outFile with write permission | |
| write msgSource to fileID | |
| close access fileID | |
| end SaveMessage | |
| -------------------------------------------------------------------------------------------- | |
| on LogEntry(someText) | |
| if not pLogging then return | |
| set logFile to (logFolder as text) & pLogFileName | |
| set logRef to open for access (file logFile) with write permission | |
| if logRef ≠ 0 then | |
| write FormatDateTime(current date) & ": " & someText & (ASCII character 10) starting at eof to logRef | |
| close access logRef | |
| end if | |
| end LogEntry | |
| -------------------------------------------------------------------------------------------- | |
| on FormatDateTime(theDate) | |
| set theDate to theDate as date | |
| set dd to text -2 thru -1 of ("0" & theDate's day) | |
| copy theDate to tempDate | |
| set the month of tempDate to January | |
| set mm to text -2 thru -1 of ¬ | |
| ("0" & 1 + (theDate - tempDate + 1314864) div 2629728) | |
| set yy to text -1 through -4 of ((year of theDate) as text) | |
| set hh to time string of theDate | |
| return (yy & "/" & mm & "/" & dd & " " & hh as text) | |
| end FormatDateTime | |
| -------------------------------------------------------------------------------------------- | |
| on SumListOfNumbers(numberList) | |
| set totalSize to 0 | |
| repeat with nextNumber in numberList | |
| set totalSize to totalSize + nextNumber | |
| end repeat | |
| return totalSize | |
| end SumListOfNumbers | |
| -------------------------------------------------------------------------------------------- | |
| on ListToString(someList) | |
| set someText to "" | |
| if (count of someList) > 0 then | |
| repeat with i from 1 to the count of someList | |
| set nextItem to item i of someList | |
| set someText to someText & nextItem | |
| if i < the (count of someList) then set someText to someText & ", " | |
| end repeat | |
| end if | |
| return someText | |
| end ListToString | |
| -------------------------------------------------------------------------------------------- | |
| on FixFilename(someText) | |
| set AppleScript's text item delimiters to {":"} | |
| set textItems to text items of someText | |
| set AppleScript's text item delimiters to {"-"} | |
| set someText to textItems as Unicode text | |
| set AppleScript's text item delimiters to {"/"} | |
| set textItems to text items of someText | |
| set AppleScript's text item delimiters to {"-"} | |
| set someText to textItems as Unicode text | |
| set AppleScript's text item delimiters to {""} | |
| return someText | |
| end FixFilename | |
| -------------------------------------------------------------------------------------------- | |
| on TrimStringToLength(someText, maxLength) | |
| if the (count of someText) ≤ maxLength then | |
| return someText | |
| else | |
| return (characters 1 through maxLength of someText) as text | |
| end if | |
| end TrimStringToLength | |
| -------------------------------------------------------------------------------------------- | |
| on FileExists(someFile) | |
| set doesExist to false | |
| try | |
| set anAlias to (someFile as alias) | |
| set doesExist to true | |
| on error | |
| set doesExist to false | |
| end try | |
| return doesExist | |
| end FileExists | |
| -------------------------------------------------------------------------------------------- | |
| on CreateFolder(parentFolder, folderName) | |
| -- add delimiter to parent Folder if needed | |
| if the last character of (parentFolder as text) is not ":" then | |
| set parentFolder to (parentFolder & ":" as text) | |
| end if | |
| -- compute folder path | |
| set folderPath to (parentFolder & folderName & ":" as text) | |
| -- determine whether folder exists | |
| if (not my FileExists(folderPath)) then | |
| -- folder does not exist - create it now | |
| tell application "Finder" | |
| set folderPath to make new folder ¬ | |
| at (parentFolder as alias) ¬ | |
| with properties {name:folderName} | |
| end tell | |
| end if | |
| -- return folder path | |
| return folderPath as text | |
| end CreateFolder | |
| -------------------------------------------------------------------------------------------- | |
| on MakeFilenameUnique(parentFolder, inName) | |
| set newName to inName | |
| set {baseFilename, fileExtension} to my SplitFilenameFromExtension(newName) | |
| set unique to not (my FileExists(parentFolder & newName as text)) | |
| if not unique then | |
| set counter to 1 | |
| repeat until unique | |
| set trimmedBaseFilename to characters 1 through ((count of baseFilename) - ((count of (counter as text)) + 1)) of baseFilename | |
| set newName to trimmedBaseFilename & "-" & counter & "." & fileExtension as text | |
| set unique to not (my FileExists(parentFolder & newName)) | |
| set counter to counter + 1 | |
| end repeat | |
| end if | |
| return newName | |
| end MakeFilenameUnique | |
| -------------------------------------------------------------------------------------------- | |
| on SplitFilenameFromExtension(filename) | |
| -- split | |
| set saveDelims to AppleScript's text item delimiters | |
| set AppleScript's text item delimiters to {"."} | |
| set itemList to (every text item of filename) | |
| set AppleScript's text item delimiters to saveDelims | |
| -- count items | |
| set numItems to the count of items in itemList | |
| -- get extension | |
| set fileExtension to item (numItems) of itemList | |
| -- get name | |
| if numItems is greater than 2 then | |
| set filename to "" | |
| repeat with i from 1 to (numItems - 1) | |
| set filename to filename & item i of itemList | |
| if i is not numItems then set filename to filename & "." | |
| end repeat | |
| else | |
| set filename to item 1 of itemList | |
| end if | |
| return {filename, fileExtension} | |
| end SplitFilenameFromExtension | |
| -------------------------------------------------------------------------------------------- | |
| on ViewLog() | |
| set alreadyViewing to ViewingLog() | |
| if alreadyViewing is not true then | |
| set logFile to (logFolder & pLogFileName as text) | |
| set terminalCommand to ("tail -f " & the quoted form of (logFile's POSIX path)) | |
| tell application "Terminal" | |
| activate | |
| set newTab to do script terminalCommand | |
| end tell | |
| end if | |
| end ViewLog | |
| -------------------------------------------------------------------------------------------- | |
| on ViewingLog() | |
| set command to "ps | grep -v grep | grep 'tail -f' | grep " & the quoted form of pLogFileName & " | awk '{print $1}'" | |
| set pid to do shell script command | |
| return (pid is not "") | |
| end ViewingLog | |
| -------------------------------------------------------------------------------------------- | |
| on DisplayAbout() | |
| tell application "System Events" | |
| activate | |
| display alert pScriptname message "This script removes duplicate messages from the current selection in Apple Mail." & return & return ¬ | |
| & "The script determines whether a message is a duplicate of another message by examining these message properties:" & return & return & ¬ | |
| "• Subject" & return & ¬ | |
| "• From" & return & ¬ | |
| "• Date" & return & ¬ | |
| "• Message ID" & return & ¬ | |
| "• Attachments" & return & return & ¬ | |
| "If a message's Subject, From, and Date headers match another message in the selection and the Message-ID header is either empty or matches the other message, the script considers the message to be a duplicate and removes the duplicate message from Mail, optionally saving the message as an archive to a file first. During this process, matching messages that have a larger total Attachment size are preferred." & return & return & ¬ | |
| "This script is provided with source code so that you may edit the script as you see fit. Read the included instructions for more information." as informational buttons {"Quit", "Continue"} default button "Continue" cancel button "Quit" | |
| end tell | |
| end DisplayAbout | |
| -------------------------------------------------------------------------------------------- | |
| on GetUserRemoveDuplicateMessagesChoice() | |
| -- interact with user | |
| tell application "System Events" | |
| activate | |
| set dResult to display alert "Remove Action" message "Remove duplicate messages from Apple Mail?" & return & return & ¬ | |
| "Click \"Remove All\" to remove all duplicate messages. Click \"Ask Me\" to have the script ask you what to do for each duplicate message. Click \"Leave In Place\" to leave duplicate messages in place." as critical buttons {"Remove All", "Leave In Place", "Ask Me"} default button "Ask Me" giving up after pDialogTimeout | |
| end tell | |
| -- process user choices | |
| set choice to dResult's button returned | |
| if choice is "Remove All" then | |
| my LogEntry("User chose to remove all duplicate messages.") | |
| return true | |
| else if choice is "Leave In Place" then | |
| my LogEntry("User chose not to remove any duplicate messages.") | |
| return false | |
| else if choice is "Ask Me" then | |
| my LogEntry("User wishes to choose whether to remove each encountered duplicate message interactively.") | |
| return "ask" | |
| end if | |
| end GetUserRemoveDuplicateMessagesChoice | |
| -------------------------------------------------------------------------------------------- | |
| on GetUserArchiveDuplicateMessagesChoice() | |
| -- interact with user | |
| tell application "System Events" | |
| activate | |
| set dResult to display alert "Archive Action" message "Save duplicate messages to the archive folder on your desktop before they are removed from Apple Mail?" & return & return & ¬ | |
| "Click \"Archive All\" to archive all duplicate messages before removal. Click \"Discard All\" to remove duplicate messages without archiving them." as critical buttons {"Discard All", "Archive All"} default button "Archive All" giving up after pDialogTimeout | |
| end tell | |
| -- process user choices | |
| set choice to dResult's button returned | |
| if choice is "Archive All" then | |
| my LogEntry("User chose to archive all duplicate messages.") | |
| return true | |
| else if choice is "Discard All" then | |
| my LogEntry("User chose not to archive any duplicate messages.") | |
| return false | |
| end if | |
| end GetUserArchiveDuplicateMessagesChoice | |
| -------------------------------------------------------------------------------------------- | |
| on GetOSVersion() | |
| set osVersion to do shell script "sw_vers -productVersion" | |
| set osVersion to osVersion as string | |
| set saveDelim to AppleScript's text item delimiters | |
| set AppleScript's text item delimiters to "." | |
| set v to the text items of osVersion | |
| set AppleScript's text item delimiters to saveDelim | |
| set versionValue to 0 | |
| repeat with i from 1 to number of items of v | |
| set temp to ((item i of v) as integer) * (100 ^ (3 - i)) as integer | |
| set versionValue to versionValue + temp | |
| end repeat | |
| return versionValue as number | |
| end GetOSVersion | |
| -------------------------------------------------------------------------------------------- | |
| on GetMailShowDuplicatesSetting() | |
| -- get OS version | |
| set osVersion to GetOSVersion() | |
| if osVersion ≥ 101000 then | |
| set keyName to "_AlwaysShowDuplicates" -- Yosemite or later | |
| else | |
| set keyName to "AlwaysShowDuplicates" -- older than Yosemite | |
| end if | |
| -- read preference setting | |
| set command to "defaults read com.apple.mail " & keyName | |
| set output to "0" -- default: not set | |
| try | |
| set output to do shell script command | |
| end try | |
| -- return true if setting is enabled | |
| return (output is "1") | |
| end GetMailShowDuplicatesSetting | |
| -------------------------------------------------------------------------------------------- | |
| on SetMailShowDuplicatesSetting(shouldShowDuplicates) | |
| set success to false -- default: failure | |
| -- get OS version | |
| set osVersion to GetOSVersion() | |
| if osVersion ≥ 101000 then | |
| set keyName to "_AlwaysShowDuplicates" -- Yosemite or later | |
| else | |
| set keyName to "AlwaysShowDuplicates" -- older than Yosemite | |
| end if | |
| -- build command | |
| if shouldShowDuplicates then | |
| my LogEntry("Showing duplicate messages in Mail.") | |
| set command to "defaults write com.apple.mail " & keyName & " -bool true" | |
| else | |
| my LogEntry("Hiding duplicate messages in Mail.") | |
| set command to "defaults write com.apple.mail " & keyName & " -bool false" | |
| end if | |
| -- execute | |
| do shell script command | |
| -- return true if setting was correctly set | |
| return (GetMailShowDuplicatesSetting() is shouldShowDuplicates) | |
| end SetMailShowDuplicatesSetting | |
| -------------------------------------------------------------------------------------------- | |
| script hashTable | |
| -- written by DJ Bazzie Wazzie at Macscripter and modified by Tim Wilson and Jolly Roger | |
| -- http://macscripter.net/viewtopic.php?id=39424 | |
| property initSize : 1000 | |
| on debug() | |
| set fileID to open for access POSIX file (POSIX path of (the path to the desktop) & "Remove Duplicate Messages Hash Table Node Listing.txt" as text) with write permission | |
| set eof of fileID to 0 | |
| repeat with x from 1 to the count of my hashList | |
| set nodeCount to the count of item x of my hashList | |
| if nodeCount > 0 then | |
| set nodeDisplay to (the count of item x of my hashList) & ": " as text | |
| repeat with node in item x of my hashList | |
| set nodeDisplay to nodeDisplay & (id of item node of my keyList) & ", " as text | |
| end repeat | |
| write nodeDisplay & (ASCII character 10) as text to fileID | |
| end if | |
| end repeat | |
| close access fileID | |
| end debug | |
| on newInstance() | |
| script hashTableInstance | |
| property parent : hashTable | |
| property size : missing value | |
| property hashList : missing value | |
| property keyList : missing value | |
| property valueList : missing value | |
| end script | |
| end newInstance | |
| on getPrimeSize() | |
| repeat with nextPrime in my PrimesToMillion | |
| if initSize < nextPrime then | |
| return nextPrime as integer | |
| exit repeat | |
| end if | |
| end repeat | |
| return initSize | |
| end getPrimeSize | |
| on init() | |
| set my size to 0 | |
| set my hashList to {} | |
| set initSize to getPrimeSize() | |
| repeat initSize times | |
| set end of my hashList to {} | |
| end repeat | |
| set my keyList to {} | |
| set my valueList to {} | |
| return me | |
| end init | |
| on initWithKeysAndValues(_keys, _values) | |
| set my keyList to _keys | |
| set my valueList to _values | |
| set my size to count my keyList | |
| set my hashList to {} | |
| set initSize to getPrimeSize() | |
| repeat initSize times | |
| set end of my hashList to {} | |
| end repeat | |
| repeat with x from 1 to my size | |
| set end of item hashFunction(item x of my keyList) of my hashList to x | |
| end repeat | |
| return me | |
| end initWithKeysAndValues | |
| on setValueforKey(_key, _value) | |
| set x to hashFunction(_key) | |
| repeat with node in item x of my hashList | |
| if id of item node of my keyList = id of _key then | |
| set item node of my valueList to _value | |
| return true | |
| end if | |
| end repeat | |
| set my size to (my size) + 1 | |
| set end of my valueList to _value | |
| set end of my keyList to _key | |
| set end of item x of my hashList to my size | |
| end setValueforKey | |
| on valueForKey(_key) | |
| set x to hashFunction(_key) | |
| repeat with node in item x of my hashList | |
| if id of item node of my keyList = id of _key then | |
| return item node of my valueList | |
| end if | |
| end repeat | |
| return missing value | |
| end valueForKey | |
| on keyExists(_key) | |
| considering case | |
| return my keyList contains _key | |
| end considering | |
| end keyExists | |
| on keys() | |
| return my keyList | |
| end keys | |
| on setKeys(_keys) | |
| set _keys to every text of _keys | |
| if (count _keys) = (count my keyList) then | |
| set my keyList to _keys | |
| --need to re-hash | |
| set my hashList to {} | |
| set initSize to getPrimeSize() | |
| repeat initSize times | |
| set end of my hashList to {} | |
| end repeat | |
| repeat with x from 1 to my size | |
| set end of item hashFunction(item x of my keyList) of my hashList to x | |
| end repeat | |
| end if | |
| end setKeys | |
| on valueExists(_value) | |
| return my valueList contains _value | |
| end valueExists | |
| on values() | |
| return my valueList | |
| end values | |
| on setValues(_values) | |
| if (count _values) = (count my valueList) then set my valueList to _values | |
| end setValues | |
| on count | |
| return my size | |
| end count | |
| on hashFunction(_key) | |
| set _hash to count _key -- Credits to Shane Stanly here, supports single character keys now. | |
| repeat with char in (id of _key as list) | |
| set _hash to _hash + char | |
| end repeat | |
| return _hash mod initSize + 1 | |
| end hashFunction | |
| property PrimesToMillion : {1009, 2521, 4133, 5851, 7639, 9437, 11321, 13183, 15139, 17077, ¬ | |
| 19087, 21061, 23027, 25087, 27073, 29153, 31253, 33331, 35393, 37501, ¬ | |
| 39659, 41777, 43913, 46093, 48281, 50363, 52571, 54727, 56897, 59051, ¬ | |
| 61331, 63541, 65731, 67957, 70313, 72469, 74729, 77017, 79279, 81457, ¬ | |
| 83717, 86083, 88397, 90599, 92831, 95107, 97423, 99721, 102019, ¬ | |
| 104393, 106699, 109049, 111431, 113761, 116089, 118429, 120817, ¬ | |
| 123059, 125399, 127763, 130087, 132523, 134909, 137143, 139511, ¬ | |
| 141811, 144341, 146743, 149101, 151423, 153739, 156131, 158597, ¬ | |
| 161009, 163433, 165931, 168449, 170809, 173191, 175757, 178117, ¬ | |
| 180317, 182851, 185267, 187559, 189989, 192499, 194933, 197369, ¬ | |
| 199807, 202289, 204821, 207269, 209569, 211891, 214483, 217069, ¬ | |
| 219523, 221909, 224351, 226817, 229247, 231589, 234259, 236813, ¬ | |
| 239357, 241783, 244297, 246707, 249089, 251543, 254053, 256577, ¬ | |
| 259151, 261713, 264211, 266701, 269057, 271549, 274123, 276557, ¬ | |
| 279127, 281641, 284161, 286673, 289283, 291779, 294391, 296969, ¬ | |
| 299623, 302123, 304537, 306949, 309541, 312229, 314603, 317159, ¬ | |
| 319511, 322009, 324589, 327023, 329591, 332039, 334661, 337189, ¬ | |
| 339769, 342319, 344873, 347341, 349919, 352411, 354883, 357611, ¬ | |
| 360181, 362867, 365357, 367789, 370609, 373229, 375707, 378379, ¬ | |
| 380957, 383611, 386117, 388813, 391247, 393629, 396269, 398857, ¬ | |
| 401539, 404197, 406951, 409477, 412001, 414677, 417239, 419693, ¬ | |
| 422353, 424861, 427529, 430081, 432559, 435059, 437653, 440371, ¬ | |
| 442903, 445297, 448157, 450707, 453421, 456037, 458789, 461437, ¬ | |
| 464143, 466747, 469267, 471749, 474379, 476849, 479489, 482099, ¬ | |
| 484727, 487507, 490019, 492707, 495269, 497801, 500369, 503197, ¬ | |
| 505657, 508367, 511057, 513683, 516361, 518801, 521401, 524063, ¬ | |
| 526601, 529241, 531919, 534581, 537133, 539899, 542567, 545267, ¬ | |
| 548039, 550631, 553249, 556037, 558611, 561103, 563999, 566639, ¬ | |
| 569323, 571871, 574501, 577123, 579737, 582419, 585043, 587539, ¬ | |
| 590243, 592993, 595513, 598193, 600949, 603569, 606181, 608863, ¬ | |
| 611531, 614143, 616741, 619373, 622109, 624763, 627619, 630467, ¬ | |
| 633037, 635659, 638431, 641279, 644107, 646637, 649283, 652039, ¬ | |
| 654611, 657361, 660053, 662693, 665239, 667999, 670777, 673403, ¬ | |
| 676061, 678833, 681403, 684121, 686879, 689411, 692221, 694867, ¬ | |
| 697457, 700331, 702983, 705559, 708161, 710863, 713467, 716291, ¬ | |
| 719177, 721733, 724441, 727061, 729719, 732467, 735169, 737843, ¬ | |
| 740533, 743273, 746287, 749051, 751739, 754343, 757259, 759881, ¬ | |
| 762529, 765151, 767773, 770459, 773159, 775963, 778667, 781367, ¬ | |
| 784097, 786803, 789473, 792151, 794743, 797473, 800213, 802951, ¬ | |
| 805537, 808459, 811147, 813907, 816689, 819437, 822067, 824669, ¬ | |
| 827231, 830131, 832681, 835553, 838393, 841091, 843793, 846661, ¬ | |
| 849253, 852079, 854927, 857573, 860357, 862919, 865681, 868337, ¬ | |
| 870931, 873913, 876497, 879269, 881953, 884717, 887459, 890161, ¬ | |
| 892951, 895651, 898361, 901249, 904181, 906779, 909289, 911969, ¬ | |
| 915017, 917767, 920467, 923227, 925733, 928621, 931387, 934121, ¬ | |
| 936907, 939749, 942367, 945179, 948041, 950809, 953567, 956387, ¬ | |
| 959323, 961943, 964693, 967511, 970247, 972847, 975619, 978491, ¬ | |
| 981439, 984323, 986981, 989921, 992633, 995461, 998117, 1000931, ¬ | |
| 1003517, 1006307, 1008913, 1011719, 1014389, 1017193, 1019927, ¬ | |
| 1022629, 1025393, 1028129, 1030949, 1033759, 1036513, 1039427, ¬ | |
| 1042183, 1044889, 1047721, 1050503, 1053491, 1056281, 1059073, ¬ | |
| 1061909, 1064743, 1067597, 1070249, 1073131, 1075727, 1078733, ¬ | |
| 1081231, 1084067, 1086901, 1089863, 1092463, 1095049, 1097891, ¬ | |
| 1100773, 1103489, 1106197, 1108781, 1111547, 1114301, 1117111, ¬ | |
| 1120051, 1122841, 1125653, 1128509, 1131191, 1133893, 1136623, ¬ | |
| 1139521, 1142357, 1145213, 1148087, 1150871, 1153597, 1156403, ¬ | |
| 1159073, 1161851, 1164433, 1167233, 1170031, 1172833, 1175717, ¬ | |
| 1178371, 1180957, 1183759, 1186489, 1189213, 1192027, 1194769, ¬ | |
| 1197617, 1200491, 1203421, 1206341, 1209337, 1212047, 1214671, ¬ | |
| 1217417, 1220249, 1223179, 1225997, 1228883, 1231589, 1234747, ¬ | |
| 1237547, 1240307, 1243309, 1246073, 1248671, 1251529, 1254427, ¬ | |
| 1257089, 1259759, 1262671, 1265461, 1268119, 1270897, 1273567, ¬ | |
| 1276621, 1279507, 1282277, 1285129, 1287787, 1290539, 1293493, ¬ | |
| 1296409, 1299269, 1302029, 1304837, 1307701, 1310473, 1313359, ¬ | |
| 1316299, 1318987, 1321867, 1324571, 1327351, 1330321, 1333151, ¬ | |
| 1336103, 1338823, 1341577, 1344359, 1347293, 1350331, 1353043, ¬ | |
| 1355867, 1358729, 1361491, 1364303, 1367161, 1370051, 1372951, ¬ | |
| 1375747, 1378387, 1381141, 1384099, 1386839, 1389797, 1392701, ¬ | |
| 1395469, 1398043, 1400687, 1403651, 1406677, 1409489, 1412239, ¬ | |
| 1415237, 1418167, 1421153, 1423969, 1426673, 1429567, 1432313, ¬ | |
| 1434997, 1437743, 1440611, 1443469, 1446251, 1449191, 1451969, ¬ | |
| 1454779, 1457501, 1460483, 1463177, 1465943, 1468933, 1471829, ¬ | |
| 1474643, 1477559, 1480277, 1483073, 1486057, 1488857, 1491583, ¬ | |
| 1494257, 1497107, 1500143, 1502933, 1505489, 1508407, 1511053, ¬ | |
| 1513891, 1516819, 1519703, 1522711, 1525423, 1528447, 1531373, ¬ | |
| 1534397, 1537177, 1540031, 1542991, 1545911, 1548647, 1551601, ¬ | |
| 1554383, 1557109, 1559879, 1563077, 1565743, 1568657, 1571587, ¬ | |
| 1574311, 1577203, 1580141, 1583107, 1585967, 1588901, 1591883, ¬ | |
| 1594783, 1597621, 1600607, 1603159, 1605907, 1608751, 1611613, ¬ | |
| 1614671, 1617647, 1620431, 1623233, 1626127, 1628839, 1631731, ¬ | |
| 1634393, 1637183, 1640399, 1643347, 1646147, 1649171, 1651921, ¬ | |
| 1654721, 1657429, 1660489, 1663327, 1666339, 1668929, 1671731, ¬ | |
| 1674733, 1677499, 1680253, 1683103, 1686119, 1689031, 1691939, ¬ | |
| 1694821, 1697719, 1700383, 1703159, 1706227, 1709033, 1711921, ¬ | |
| 1714859, 1717787, 1720633, 1723669, 1726513, 1729363, 1732361, ¬ | |
| 1735301, 1738283, 1740917, 1743919, 1746761, 1749413, 1752239, ¬ | |
| 1755043, 1758073, 1761187, 1764221, 1767121, 1769947, 1772959, ¬ | |
| 1775687, 1778477, 1781341, 1784213, 1787087, 1790149, 1792891, ¬ | |
| 1795669, 1798619, 1801363, 1804447, 1807153, 1809917, 1812823, ¬ | |
| 1815691, 1818743, 1821707, 1824463, 1827193, 1830029, 1832819, ¬ | |
| 1835767, 1838933, 1841929, 1844659, 1847473, 1850441, 1853329, ¬ | |
| 1855961, 1858861, 1861709, 1864591, 1867241, 1870223, 1873133, ¬ | |
| 1876073, 1878841, 1881757, 1884721, 1887539, 1890379, 1893197, ¬ | |
| 1896149, 1899047, 1902119, 1905031, 1907963, 1910737, 1913533, ¬ | |
| 1916333, 1919293, 1922269, 1925179, 1927813, 1930603, 1933681, ¬ | |
| 1936621, 1939489, 1942273, 1945129, 1947971, 1950989, 1953863, ¬ | |
| 1956737, 1959751, 1962551, 1965571, 1968563, 1971667, 1974779, ¬ | |
| 1977709, 1980469, 1983323, 1986277, 1989107, 1992041, 1994953, ¬ | |
| 1997657, 2000429, 2003459, 2006317, 2008973, 2012011, 2014799, ¬ | |
| 2017669, 2020591, 2023369, 2026469, 2029249, 2032273, 2034913, ¬ | |
| 2037851, 2040791, 2043703, 2046731, 2049791, 2052769, 2055637, ¬ | |
| 2058541, 2061361, 2064199, 2067019, 2069983, 2072933, 2075929, ¬ | |
| 2078971, 2081873, 2084921, 2087671, 2090497, 2093393, 2096569, ¬ | |
| 2099453, 2102279, 2105149, 2108033, 2110891, 2113939, 2116729, ¬ | |
| 2119681, 2122691, 2125691, 2128463, 2131399, 2134283, 2137159, ¬ | |
| 2140139, 2143027, 2145707, 2148733, 2151703, 2154667, 2157401, ¬ | |
| 2160113, 2163071, 2166077, 2169029, 2172067, 2174941, 2177699, ¬ | |
| 2180701, 2183791, 2186689, 2189513, 2192563, 2195383, 2198419, ¬ | |
| 2201519, 2204393, 2207251, 2210057, 2212963, 2216113, 2219141, ¬ | |
| 2222251, 2225077, 2227843, 2230721, 2233717, 2236483, 2239537, ¬ | |
| 2242549, 2245457, 2248333, 2251489, 2254403, 2257097, 2259967, ¬ | |
| 2262889, 2265751, 2268547, 2271337, 2274101, 2277071, 2280071, ¬ | |
| 2283301, 2286233, 2289163, 2292071, 2295053, 2297983, 2300813, ¬ | |
| 2303681, 2306761, 2309519, 2312603, 2315597, 2318623, 2321701, ¬ | |
| 2324639, 2327597, 2330617, 2333549, 2336381, 2339429, 2342257, ¬ | |
| 2345261, 2348321, 2351317, 2354251, 2357297, 2360087, 2363041, ¬ | |
| 2365829, 2368759, 2371771, 2374751, 2377751, 2380733, 2383517, ¬ | |
| 2386493, 2389379, 2392163, 2395357, 2398171, 2401031, 2404009, ¬ | |
| 2407049, 2410153, 2413123, 2415997, 2418733, 2421673, 2424743, ¬ | |
| 2427599, 2430671, 2433721, 2436607, 2439781, 2442589, 2445437, ¬ | |
| 2448443, 2451377, 2454113, 2457179, 2459953, 2462917, 2465789, ¬ | |
| 2468861, 2471827, 2474863, 2477899, 2480873, 2483869, 2486681, ¬ | |
| 2489611, 2492599, 2495567, 2498449, 2501489, 2504407, 2507233, ¬ | |
| 2510309, 2513113, 2515951, 2519171, 2522257, 2524859, 2527913, ¬ | |
| 2530573, 2533651, 2536657, 2539543, 2542699, 2545651, 2548697, ¬ | |
| 2551723, 2554481, 2557567, 2560427, 2563343, 2566423, 2569297, ¬ | |
| 2572231, 2575123, 2577889, 2580803, 2583547, 2586611, 2589479, ¬ | |
| 2592581, 2595421, 2598367, 2601437, 2604509, 2607529, 2610583, ¬ | |
| 2613503, 2616209, 2619223, 2622343, 2625169, 2628341, 2631137, ¬ | |
| 2633783, 2636971, 2640017, 2642971, 2645749, 2648669, 2651617, ¬ | |
| 2654609, 2657327, 2660303, 2663459, 2666141, 2669281, 2672497, ¬ | |
| 2675441, 2678441, 2681381, 2684299, 2687239, 2690089, 2693111, ¬ | |
| 2695999, 2698807, 2701871, 2704909, 2707739, 2710681, 2713603, ¬ | |
| 2716579, 2719219, 2722469, 2725517, 2728669, 2731607, 2734549, ¬ | |
| 2737841, 2740631, 2743523, 2746603, 2749567, 2752517, 2755663, ¬ | |
| 2758589, 2761477, 2764313, 2767321, 2770387, 2773249, 2776259, ¬ | |
| 2779303, 2782279, 2785177, 2788213, 2791409, 2794283, 2797387, ¬ | |
| 2800079, 2803067, 2805911, 2808877, 2811569, 2814431, 2817443, ¬ | |
| 2820331, 2823133, 2826113, 2829383, 2832257, 2834873, 2837803, ¬ | |
| 2840771, 2843693, 2846869, 2849779, 2852779, 2855603, 2858777, ¬ | |
| 2861801, 2864749, 2867677, 2870761, 2873707, 2876869, 2879999, ¬ | |
| 2882863, 2885917, 2888843, 2891789, 2894863, 2897981, 2900831, ¬ | |
| 2903909, 2906803, 2909633, 2912803, 2915849, 2918777, 2921729, ¬ | |
| 2924573, 2927707, 2930651, 2933753, 2936683, 2939401, 2942699, ¬ | |
| 2945731, 2948861, 2951687, 2954683, 2957897, 2960801, 2963683, ¬ | |
| 2966723, 2969591, 2972693, 2975569, 2978681, 2981483, 2984441, ¬ | |
| 2987407, 2990609, 2993447, 2996449, 2999287, 3002369, 3005179, ¬ | |
| 3008191, 3011209, 3014119, 3017281, 3020351, 3023417, 3026351, ¬ | |
| 3029249, 3032377, 3035419, 3038353, 3041531, 3044347, 3047411, ¬ | |
| 3050473, 3053443, 3056531, 3059467, 3062467, 3065779, 3068749, ¬ | |
| 3071569, 3074419, 3077281, 3080263, 3083203, 3086177, 3089029, ¬ | |
| 3091897, 3094951, 3097999, 3101359, 3104219, 3107113, 3110011, ¬ | |
| 3113129, 3116039, 3118789, 3121831, 3124883, 3127907, 3130823, ¬ | |
| 3133853, 3136877, 3139901, 3142963, 3146029, 3148991, 3151913, ¬ | |
| 3154717, 3157613, 3160909, 3163969, 3166661, 3169847, 3172667, ¬ | |
| 3175691, 3178759, 3181861, 3184637, 3187669, 3190553, 3193471, ¬ | |
| 3196489, 3199379, 3202183, 3205219, 3208043, 3210959, 3214147, ¬ | |
| 3216931, 3219989, 3222953, 3225947, 3228943, 3231953, 3234871, ¬ | |
| 3237643, 3240619, 3243859, 3246769, 3249871, 3252859, 3255929, ¬ | |
| 3258911, 3261737, 3264893, 3267773, 3270797, 3273989, 3276781, ¬ | |
| 3279709, 3282857, 3285889, 3288797, 3291707, 3294833, 3297781, ¬ | |
| 3301027, 3304237, 3306971, 3309923, 3312893, 3315883, 3319031, ¬ | |
| 3322069, 3325027, 3327991, 3330979, 3334117, 3337283, 3340193, ¬ | |
| 3342943, 3346151, 3349097, 3352177, 3355337, 3358559, 3361667, ¬ | |
| 3364763, 3367571, 3370363, 3373499, 3376567, 3379357, 3382427, ¬ | |
| 3385441, 3388243, 3391343, 3394487, 3397351, 3400457, 3403259, ¬ | |
| 3406259, 3409261, 3412099, 3415051, 3418117, 3421169, 3424271, ¬ | |
| 3427187, 3430337, 3433211, 3436241, 3439003, 3441943, 3444817, ¬ | |
| 3447971, 3450773, 3454093, 3457087, 3460213, 3463319, 3466409, ¬ | |
| 3469387, 3472549, 3475559, 3478771, 3481823, 3485047, 3488209, ¬ | |
| 3491249, 3494173, 3497243, 3500327, 3503477, 3506291, 3509549, ¬ | |
| 3512417, 3515371, 3518659, 3521627, 3524779, 3528043, 3531097, ¬ | |
| 3533963, 3536959, 3540161, 3543349, 3546281, 3549307, 3552427, ¬ | |
| 3555427, 3558409, 3561407, 3564487, 3567373, 3570311, 3573473, ¬ | |
| 3576421, 3579647, 3582671, 3585689, 3588499, 3591613, 3594763, ¬ | |
| 3597569, 3600601, 3603623, 3606563, 3609391, 3612277, 3615103, ¬ | |
| 3618071, 3621223, 3623999, 3626947, 3629933, 3632953, 3636053, ¬ | |
| 3639197, 3642101, 3645349, 3648451, 3651763, 3654947, 3657691, ¬ | |
| 3660871, 3663893, 3666841, 3669881, 3672793, 3675817, 3678539, ¬ | |
| 3681553, 3684677, 3687763, 3690727, 3693787, 3696557, 3699517, ¬ | |
| 3702529, 3705511, 3708643, 3711857, 3714769, 3717803, 3720707, ¬ | |
| 3723781, 3726623, 3729461, 3732343, 3735517, 3738401, 3741671, ¬ | |
| 3744761, 3747929, 3750737, 3753653, 3756629, 3759781, 3763241, ¬ | |
| 3766261, 3769331, 3772547, 3775507, 3778447, 3781507, 3784481, ¬ | |
| 3787471, 3790729, 3793649, 3796847, 3799717, 3802787, 3805679, ¬ | |
| 3808999, 3811763, 3814739, 3817657, 3820589, 3823433, 3826621, ¬ | |
| 3829633, 3832613, 3835763, 3838883, 3841729, 3844823, 3847783, ¬ | |
| 3850831, 3853909, 3856949, 3859939, 3862987, 3866099, 3869113, ¬ | |
| 3872381, 3875363, 3878521, 3881413, 3884537, 3887549, 3890507, ¬ | |
| 3893359, 3896287, 3899381, 3902531, 3905777, 3908581, 3911507, ¬ | |
| 3914747, 3917801, 3920747, 3923867, 3926789, 3929633, 3932717, ¬ | |
| 3935837, 3938783, 3941837, 3944729, 3947807, 3950693, 3953879, ¬ | |
| 3957007, 3960083, 3963301, 3966539, 3969557, 3972533, 3975463, ¬ | |
| 3978283, 3981503, 3984751, 3987859, 3991087, 3993959, 3997297, ¬ | |
| 4000343, 4003231, 4006481, 4009373, 4012297, 4015381, 4018457, ¬ | |
| 4021601, 4024507, 4027501, 4030553, 4033577, 4036391, 4039249, ¬ | |
| 4042303, 4045229, 4048199, 4051181, 4054283, 4057633, 4060591, ¬ | |
| 4063583, 4066759, 4069699, 4072841, 4075717, 4078663, 4081669, ¬ | |
| 4084937, 4087807, 4090859, 4093861, 4096871, 4099889, 4103173, ¬ | |
| 4106117, 4109101, 4112051, 4115269, 4118417, 4121591, 4124671, ¬ | |
| 4127749, 4130837, 4134203, 4137299, 4140377, 4143353, 4146383, ¬ | |
| 4149437, 4152373, 4155467, 4158403, 4161407, 4164179, 4167239, ¬ | |
| 4170371, 4173493, 4176587, 4179607, 4182593, 4185617, 4188551, ¬ | |
| 4191491, 4194601, 4197667, 4200739, 4203841, 4206659, 4209817, ¬ | |
| 4212727, 4215899, 4218979, 4221941, 4224977, 4228141, 4231309, ¬ | |
| 4234229, 4237631, 4240711, 4243573, 4246807, 4249807, 4252747, ¬ | |
| 4255697, 4258829, 4261949, 4264901, 4268171, 4271177, 4274177, ¬ | |
| 4277341, 4280363, 4283581, 4286719, 4289921, 4292903, 4296077, ¬ | |
| 4299157, 4302167, 4305073, 4308251, 4311337, 4314281, 4317409, ¬ | |
| 4320341, 4323691, 4326671, 4329581, 4332707, 4335979, 4338871, ¬ | |
| 4341877, 4345063, 4348193, 4351297, 4354297, 4357519, 4360663, ¬ | |
| 4363607, 4366811, 4369801, 4372777, 4375697, 4379021, 4381961, ¬ | |
| 4384871, 4387969, 4391087, 4394249, 4397311, 4400497, 4403569, ¬ | |
| 4406653, 4409897, 4412897, 4415813, 4418797, 4421743, 4424653, ¬ | |
| 4427623, 4430593, 4433629, 4436749, 4439663, 4442441, 4445527, ¬ | |
| 4448557, 4451537, 4454551, 4457611, 4460657, 4463729, 4466873, ¬ | |
| 4469831, 4472957, 4476041, 4478989, 4482131, 4484971, 4487849, ¬ | |
| 4490987, 4494167, 4497151, 4500281, 4503371, 4506251, 4509457, ¬ | |
| 4512749, 4515881, 4518907, 4522027, 4525253, 4528253, 4531321, ¬ | |
| 4534487, 4537711, 4540769, 4543871, 4546793, 4550059, 4553111, ¬ | |
| 4555867, 4559153, 4562219, 4565047, 4568089, 4571107, 4574441, ¬ | |
| 4577413, 4580413, 4583521, 4586633, 4589731, 4592657, 4595713, ¬ | |
| 4598749, 4602041, 4605017, 4608007, 4610917, 4613971, 4617271, ¬ | |
| 4620149, 4623379, 4626299, 4629523, 4632673, 4635703, 4638691, ¬ | |
| 4641739, 4645061, 4647887, 4651027, 4654231, 4657571, 4660637, ¬ | |
| 4663657, 4666813, 4669681, 4672751, 4675733, 4678943, 4681763, ¬ | |
| 4684817, 4687637, 4690859, 4694003, 4696871, 4700057, 4703057, ¬ | |
| 4706101, 4709291, 4712443, 4715519, 4718491, 4721513, 4724747, ¬ | |
| 4727711, 4730839, 4733623, 4736737, 4739857, 4742891, 4746047, ¬ | |
| 4749191, 4752347, 4755481, 4758673, 4761553, 4764779, 4767757, ¬ | |
| 4770737, 4774261, 4777139, 4780409, 4783291, 4786477, 4789549, ¬ | |
| 4792829, 4795913, 4798933, 4802011, 4804781, 4807951, 4811171, ¬ | |
| 4814191, 4817339, 4820237, 4823303, 4826713, 4829749, 4832833, ¬ | |
| 4835801, 4838551, 4841737, 4844977, 4848037, 4851109, 4854193, ¬ | |
| 4857329, 4860343, 4863569, 4866503, 4869433, 4872431, 4875677, ¬ | |
| 4878851, 4881763, 4884827, 4887901, 4890709, 4893689, 4896637, ¬ | |
| 4899619, 4902571, 4905917, 4908713, 4911943, 4915067, 4918477, ¬ | |
| 4921289, 4924397, 4927423, 4930543, 4933583, 4936693, 4939769, ¬ | |
| 4943009, 4945849, 4949029, 4952089, 4955081, 4957969, 4961321, ¬ | |
| 4964261, 4967167, 4970237, 4973357, 4976453, 4979353, 4982501, ¬ | |
| 4985597, 4988437, 4991417, 4994471, 4997891, 5000923} | |
| end script | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment