Skip to content

Instantly share code, notes, and snippets.

@extratone
Created February 15, 2023 15:50
Show Gist options
  • Select an option

  • Save extratone/a04208125528bef6ace590665500b6b7 to your computer and use it in GitHub Desktop.

Select an option

Save extratone/a04208125528bef6ace590665500b6b7 to your computer and use it in GitHub Desktop.
(*
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