mail_deduplicate packageΒΆ
Expose package-wide elements.
- class mail_deduplicate.StrEnum(new_class_name, /, names, *, module=None, qualname=None, type=None, start=1, boundary=None)[source]ΒΆ
-
Enum where members are also (and must be) strings
SubmodulesΒΆ
mail_deduplicate.action moduleΒΆ
Actions performed once the selection is settled: copy, move, delete or hardlink.
Each action ID pairs an operation verb with the subset of mails it applies to, and
Action.perform() routes one to the other.
- mail_deduplicate.action.dry_run_prefix(dedup)[source]ΒΆ
Marks a summary as describing what a run would have done.
A dry run reports through the same trail as a real one, so what it did not do is said once at the end rather than warned about for every single mail.
- Return type:
- mail_deduplicate.action.copy_mails(dedup, mails)[source]ΒΆ
Copy provided
mailsto a brand new box or an existing one.- Return type:
- mail_deduplicate.action.move_mails(dedup, mails)[source]ΒΆ
Move provided
mailsto a brand new box or an existing one.- Return type:
- mail_deduplicate.action.delete_mails(dedup, mails)[source]ΒΆ
Remove provided
mailsin-place, from their original boxes.- Return type:
- mail_deduplicate.action.LINK_TEMP_PREFIX = '.mdedup-hardlink-'ΒΆ
Prefix of the temporary link a mail is replaced through.
Every folder-based format skips dot-prefixed files when listing its mails, so a temporary left behind by an interrupted run is never read back as one.
- mail_deduplicate.action.has_own_file(mail)[source]ΒΆ
Whether the mail is backed by a file holding it alone.
File-based boxes pack all their mails into the boxβs single file, which is what
pathreturns for each of them: there would be nothing to link but the whole box.- Return type:
- mail_deduplicate.action.hardlink_blocker(dedup, mail, target)[source]ΒΆ
Explain why a mail cannot be replaced by a hardlink to its target.
Returns the reason as a sentence ready to be logged, or
Nonewhen the link can go ahead. Nothing is touched along the way, so a dry run reaches the same verdicts a real run acts on.
- mail_deduplicate.action.replace_by_hardlink(mail_path, target_path)[source]ΒΆ
Point a mailβs own path at the file backing another mail.
The link is created under a temporary name in the mailβs own directory, then renamed over it: the rename is atomic and shares the mailβs filesystem by construction, so the mail is never missing from its box, whatever interrupts the run. The mail also keeps its file name, which is where
maildirrecords the per-folder flags that make the same mail read in one folder and unread in another.- Return type:
- mail_deduplicate.action.hardlink_mails(dedup, mails)[source]ΒΆ
Replace provided
mailsin-place by a hardlink to the copy kept in their set.The mails stay right where they are, under their own name: only the content they are backed by is shared with the copy that survived the selection, so the disk space they took is reclaimed.
- Return type:
- mail_deduplicate.action.OPERATIONS: dict[str, Callable[[Deduplicate, Collection[DedupMailMixin]], None]] = {'copy': <function copy_mails>, 'delete': <function delete_mails>, 'hardlink': <function hardlink_mails>, 'move': <function move_mails>}ΒΆ
The operation functions above, keyed by the verb half of an action ID.
All share the same signature: the deduplication they report to, and the mails they apply to.
- class mail_deduplicate.action.Action(*values)[source]ΒΆ
Bases:
StrEnumDefine all available action IDs.
An action ID joins an operation verb to the subset of mails it applies to: the
*-selectedactions act on the mails kept by the selection, the*-discardedones on the mails it discarded.- COPY_SELECTED = 'copy-selected'ΒΆ
- COPY_DISCARDED = 'copy-discarded'ΒΆ
- MOVE_SELECTED = 'move-selected'ΒΆ
- MOVE_DISCARDED = 'move-discarded'ΒΆ
- DELETE_SELECTED = 'delete-selected'ΒΆ
- DELETE_DISCARDED = 'delete-discarded'ΒΆ
- HARDLINK_DISCARDED = 'hardlink-discarded'ΒΆ
- property acts_on_discarded: boolΒΆ
Whether the action applies to the discarded mails rather than the selected ones.
mail_deduplicate.cache moduleΒΆ
Persistent cache of mail hashes, so a second run does not re-parse unchanged mails.
Hashing dominates a run, and it is pure work: the same mail, hashed with the same settings, always produces the same result. Storing that result lets a later run skip opening and parsing every mail it has already seen. See: https://github.com/kdeldycke/mail-deduplicate/issues/87
Correctness rests on two independent guards:
A fingerprint of every setting feeding the hash. Anything else invalidates the whole database, as no entry produced under different settings can be trusted.
A per-mail staleness key of
(size, mtime), taken from the file backing the mail before it is read, so a mail modified mid-run is re-hashed by the next one instead of being trusted.
- mail_deduplicate.cache.SCHEMA_VERSION = '1'ΒΆ
Bumped whenever the table layout changes, to discard databases of older shapes.
- mail_deduplicate.cache.CACHED_SETTINGS = ('hash_headers', 'hash_body', 'time_source')ΒΆ
Configuration keys whose value changes what gets cached.
hash_headersandhash_bodychange the hash itself.time_sourcechanges the timestamp memoized alongside it. The minimal-headers floor is derived from the number of hash headers, so it is covered by the first one.
- class mail_deduplicate.cache.StaleKey(size: int, mtime_ns: int)[source]ΒΆ
Bases:
NamedTupleIdentifies the version of the file backing a mail.
Create new instance of StaleKey(size, mtime_ns)
- class mail_deduplicate.cache.CacheEntry(mail_hash: str, timestamp: float | None, mail_size: int | None)[source]ΒΆ
Bases:
NamedTupleWhat is worth keeping about a hashed mail.
The hash is the point. The two scalars are what
dehydrate()memoizes to spare the later steps a re-read, so restoring them is what actually lets a cached mail stay unparsed.mail_sizeis only known when the body was hashed, andtimestampisNonefor a mail whoseDateheader could not be parsed.Create new instance of CacheEntry(mail_hash, timestamp, mail_size)
- mail_deduplicate.cache.default_cache_dir()[source]ΒΆ
Location of the cache database, following each platformβs conventions.
~/Library/Caches/mdedupon macOS,$XDG_CACHE_HOME/mdedupon other POSIX systems, and%LOCALAPPDATA%\mdedup\Cacheon Windows.appauthoris turned off because there is no vendor to namespace under: leaving it unset would have Windows fall back to the application name and nest the cache one level deeper, undermdedup\mdedup\Cache.- Return type:
- mail_deduplicate.cache.settings_fingerprint(conf)[source]ΒΆ
Digest of every setting that changes what a cached entry would hold.
- Return type:
- mail_deduplicate.cache.open_cache(conf)[source]ΒΆ
Opens the hash cache the configuration asks for, if it asks for one at all.
A cache only ever saves work, so a database that cannot be opened, on a read-only or full filesystem for instance, is reported and skipped instead of taking the whole run down with it.
- class mail_deduplicate.cache.HashCache(path, fingerprint)[source]ΒΆ
Bases:
objectA mail hash cache backed by SQLite, keyed by mail identity.
Opened for the whole run and committed once at the end, so an interrupted run leaves the database as it found it.
- WRITE_BATCH: int = 512ΒΆ
Rows buffered before being handed to SQLite in one
executemany().Inserting one row at a time costs more in statement overhead than in storage.
- LOCK_TIMEOUT: float = 30.0ΒΆ
Seconds a run waits for another one to release the database.
Runs sharing a database only ever contend on the single write burst each of them performs at the end, so waiting it out beats failing. See
commit()for what happens when the wait is not enough.
- lookup(box, mail_id)[source]ΒΆ
Returns the entry cached for a mail, if it is still valid.
The staleness key is taken here, before the caller reads the mail, so a mail modified between this check and its hashing is caught by the next run rather than trusted.
- Return type:
- store(source_path, mail_id, entry)[source]ΒΆ
Record the hash of a freshly read mail, under the key taken by
lookup().Silently skipped for a mail that was never looked up, as there is then no staleness key that provably predates the read.
- Return type:
- prune()[source]ΒΆ
Drops the entries of mails and boxes that are no longer there.
Two kinds of leftovers accumulate. A box that was deleted or moved keeps every one of its entries, and a mail deleted from a box that is still around keeps its own. Both are dropped here, and how many were removed is recorded as
prunedfor the run to report.Only the boxes visited by this run are considered for their individual mails: every mail of a box that was not opened is missing from
seenfor the plain reason that nobody looked, which is not evidence that it is gone.- Return type:
- forget(source_path, mail_id)[source]ΒΆ
Drop the staleness key of a mail that will not be recorded.
- Return type:
- commit()[source]ΒΆ
Flush everything recorded during the run, in one transaction.
Returns whether it went through. By this point the deduplication itself is done, so a database another run is holding, or a disk that filled up while we worked, costs the next run its head start and nothing more: it must not take down a run that has already produced its results.
- Return type:
mail_deduplicate.cli moduleΒΆ
The mdedup command: its options, their validation, and the Config mapping
carried through the whole run.
- mail_deduplicate.cli.DEFAULT_HASH_HEADERS: tuple[str, ...] = ('Date', 'From', 'To', 'Subject', 'MIME-Version', 'Content-Type', 'Content-Disposition', 'User-Agent', 'X-Priority', 'Message-ID')ΒΆ
Default ordered list of headers to use to compute the unique hash of a mail.
By default we choose to exclude:
CCSince
mailmanapparently sometimes trims list members from theCCheader to avoid sending duplicates. Which means that copies of mail reflected back from the list server will have a differentCCto the copy saved by the MUA at send-time.BCCBecause copies of the mail saved by the MUA at send-time will have
BCC, but copies reflected back from the list server wonβt.Reply-ToSince a mail could be
CCβd to two lists with differentReply-Tomunging options set.
- mail_deduplicate.cli.DEFAULT_MINIMAL_HEADERS = 4ΒΆ
Cap on the number of headers that must be present in a mail to compute a solid hash.
The per-mail floor is
min(DEFAULT_MINIMAL_HEADERS, len(hash_headers)): it rejects near-empty or corrupted mails whose hash would rest on too few headers, while relaxing automatically when the hash is narrowed to fewer headers than this cap via--hash-header.
- class mail_deduplicate.cli.Config[source]ΒΆ
Bases:
TypedDictHolds global configuration.
- hash_body: BodyHasherΒΆ
- time_source: TimeSourceΒΆ
- mail_deduplicate.cli.normalize_headers(ctx, param, value)[source]ΒΆ
Validate headers provided as parameters to the CLI.
Headers are case-insensitive in Python implementation, so we normalize them to lower-case.
We then deduplicate them, while preserving order.
Mail headers are expected to be composed of ASCII characters between 33 and 126 (both inclusive) according to RFC-5322.
- mail_deduplicate.cli.unique_strategies(ctx, param, value)[source]ΒΆ
Deduplicate strategies provided as parameters to the CLI, preserving order.
Strategies are deduplicated by the selection function they point to, so repeating a strategy already listed under one of its aliases is ignored too.
- mail_deduplicate.cli.compile_regexp(ctx, param, value)[source]ΒΆ
Validate and compile regular expression provided as parameters to the CLI.
- mail_deduplicate.cli.PATH_STRATEGIES = (Strategy.DISCARD_MATCHING_PATH, Strategy.DISCARD_NON_MATCHING_PATH, Strategy.SELECT_MATCHING_PATH, Strategy.SELECT_NON_MATCHING_PATH)ΒΆ
Strategies relying on the
-r/--regexpparameter.
- mail_deduplicate.cli.EXPORT_ACTIONS = (Action.COPY_SELECTED, Action.COPY_DISCARDED, Action.MOVE_SELECTED, Action.MOVE_DISCARDED)ΒΆ
Actions relying on the
-E/--exportparameter.
- mail_deduplicate.cli.human_join(items)[source]ΒΆ
Renders IDs as an English enumeration, for the help screen.
Both tuples above are the single source of truth of which strategies and actions take an extra parameter, so the options mentioning them read the list from there instead of spelling it out again and drifting from it.
- Return type:
- class mail_deduplicate.cli.AnyValueIn(param_name, targets, label, negate=False)[source]ΒΆ
Bases:
PredicateCondition that is true when any of a parameterβs values is in a target set.
The parameterβs resolved value is inspected, so defaults count, not only user-provided values. Single values and
multiple=Truetuples are normalized to a common shape. Setnegateto invert the membership test: true when none of the values is in the target set.Whatever
negatesays, the condition silently evaluates to false when no mail source is provided: the CLI then only prints its help screen and exits, so no parameter combination deserves a validation error.The same applies in
-H/--hash-onlymode: selection and action steps never run there, so options they require (like--exportfor the default copy-selected action) must not be demanded. Options from those steps are separately reported as ignored by the hash-only code path.
- class mail_deduplicate.cli.MdedupCommand(*args, version_fields=None, config_schema=None, config_strict=False, schema_strict=False, fallback_sections=(), config_validators=(), included_params=None, excluded_params=None, extra_option_at_end=True, populate_auto_envvars=True, extra_keywords=None, excluded_keywords=None, **kwargs)[source]ΒΆ
Bases:
CommandList of extra parameters:
- Parameters:
version_fields (
dict[str,Any] |None) β dictionary ofVersionOptiontemplate field overrides forwarded to the version option. Accepts any field fromVersionOption.template_fields(likeprog_name,version,git_branch). Lets you customize--versionoutput from the command decorator without replacing the defaultparamslist.config_strict (
bool) β forwarded to the defaultConfigOptionβsstrictsetting: configuration keys not matching any CLI parameter raise an error instead of being silently ignored. Like the otherconfig_*and*_paramsforwards, it spares you from replacing the whole defaultparamslist to customize the config option.excluded_params (
Sequence[str] |None) β additional parameter IDs to block from configuration files, merged into the defaultConfigOptionβsexcluded_paramsblocklist. Additive, unlike the option-levelexcluded_paramswhich replaces the default blocklist entirely. Items are fully-qualified parameter IDs (likemycli.mail_sources). Mutually exclusive withincluded_params.extra_keywords (
HelpKeywords|None) β aHelpKeywordsinstance whose entries are merged into the auto-collected keyword set. Use this to inject additional strings for help screen highlighting.excluded_keywords (
HelpKeywords|None) β aHelpKeywordsinstance whose entries are removed from the auto-collected keyword set. Use this to suppress highlighting of specific strings.extra_option_at_end (
bool) β reorders all parameters attached to the command, by moving all instances ofExtraOptionat the end of the parameter list. The original order of the options is preserved among themselves.populate_auto_envvars (
bool) β forces all parameters to have their auto-generated environment variables registered. This address the shortcoming ofclickwhich only evaluates them dynamically. By forcing their registration, the auto-generated environment variables gets displayed in the help screen, fixing click#2483 issue. On Windows, environment variable names are case-insensitive, so we normalize them to uppercase.
By default, these Click context settings are applied:
auto_envvar_prefix = self.name(Click feature)Auto-generate environment variables for all options, using the command ID as prefix. The prefix is normalized to be uppercased and all non-alphanumerics replaced by underscores.
help_option_names = ("--help", "-h")(Click feature)Allow help screen to be invoked with either βhelp or -h options.
show_default = True(Click feature)Show all default values in help screen.
Additionally, these Cloup context settings are set:
align_option_groups = False(Cloup feature)show_constraints = True(Cloup feature)show_subcommand_aliases = True(Cloup feature)
Click Extra also adds its own
context_settings:show_choices = None(Click Extra feature)If set to
TrueorFalse, will force that value on all options, so we can globally show or hide choices when prompting a user for input. Only makes sense for options whosepromptproperty is set.Defaults to
None, which will leave all options untouched, and let them decide of their ownshow_choicessetting.show_envvar = None(Click Extra feature)If set to
TrueorFalse, will force that value on all options, so we can globally enable or disable the display of environment variables in help screen.Defaults to
None, which will leave all options untouched, and let them decide of their ownshow_envvarsetting. The rationale being that discoverability of environment variables is enabled by the--paramsoption, which is active by default on extra commands. So there is no need to surcharge the help screen.This addresses the click#2313 issue.
To override these defaults, you can pass your own settings with the
context_settingsparameter:@command( context_settings={ "show_default": False, ... } )
mail_deduplicate.deduplicate moduleΒΆ
The deduplication pipeline: group mails by hash, settle each duplicate set, keep score.
The Deduplicate orchestrator drives the whole run. Both of its expensive steps,
hashing and selection, can fan out across worker processes: the module-level
_-prefixed functions are the halves that run inside a worker.
- class mail_deduplicate.deduplicate.Stat(*values)[source]ΒΆ
Bases:
EnumAll tracked statistics.
The memberβs name carries the category as its
MAIL_/SET_prefix, and its value the description shown in the final report.- MAIL_FOUND = 'Total number of mails encountered from all mail sources.'ΒΆ
- MAIL_REJECTED = 'Number of mails rejected individually because they were unparsable or did not have enough metadata to compute hashes.'ΒΆ
- MAIL_RETAINED = 'Number of valid mails parsed and retained for deduplication.'ΒΆ
- MAIL_HASHES = 'Number of unique hashes.'ΒΆ
- MAIL_UNIQUE = 'Number of unique mails (which were automatically added to selection).'ΒΆ
- MAIL_DUPLICATES = 'Number of duplicate mails (sum of mails in all duplicate sets with at least 2 mails).'ΒΆ
- MAIL_SKIPPED = 'Number of mails ignored in the selection step because the whole set they belong to was skipped.'ΒΆ
- MAIL_DISCARDED = 'Number of mails discarded from the final selection.'ΒΆ
- MAIL_SELECTED = 'Number of mails kept in the final selection on which the action will be performed.'ΒΆ
- MAIL_COPIED = 'Number of mails copied from their original mailbox to another.'ΒΆ
- MAIL_MOVED = 'Number of mails moved from their original mailbox to another.'ΒΆ
- MAIL_DELETED = 'Number of mails deleted from their mailbox in-place.'ΒΆ
- MAIL_HARDLINKED = 'Number of mails replaced in-place by a hardlink to the copy kept in their duplicate set.'ΒΆ
- MAIL_HARDLINK_SKIPPED = 'Number of mails left untouched by the hardlinking action, because they could not be linked to their copy or already were.'ΒΆ
- SET_TOTAL = 'Total number of duplicate sets.'ΒΆ
- SET_SINGLE = 'Total number of sets containing only a single mail with no applicable strategy. They were automatically kept in the final selection.'ΒΆ
- SET_SKIPPED_ENCODING = 'Number of sets skipped from the selection process because they had encoding issues.'ΒΆ
- SET_SKIPPED_SIZE = 'Number of sets skipped from the selection process because they were too dissimilar in size.'ΒΆ
- SET_SKIPPED_CONTENT = 'Number of sets skipped from the selection process because they were too dissimilar in content.'ΒΆ
- SET_SKIPPED_TIMESTAMP = 'Number of sets skipped from the selection process because a timestamp could not be derived for some of their mails.'ΒΆ
- SET_SKIPPED_STRATEGY = 'Number of sets skipped from the selection process because the strategy could not be applied.'ΒΆ
- SET_DEDUPLICATED = 'Number of valid sets on which the selection strategy was successfully applied.'ΒΆ
- exception mail_deduplicate.deduplicate.SizeDiffAboveThreshold[source]ΒΆ
Bases:
ExceptionDifference in mail size is greater than threshold.
- exception mail_deduplicate.deduplicate.ContentDiffAboveThreshold[source]ΒΆ
Bases:
ExceptionDifference in mail content is greater than threshold.
- exception mail_deduplicate.deduplicate.MissingTimestamps[source]ΒΆ
Bases:
ExceptionSome mails of a duplicate set have no timestamp, so they cannot be compared by time-based strategies.
Happens for mails without a parseable
Dateheader, when the timestamp is sourced from it.
- class mail_deduplicate.deduplicate.BodyHasher(*values)[source]ΒΆ
Bases:
StrEnumEnumeration of available body hashing methods.
- SKIP = 'skip'ΒΆ
- RAW = 'raw'ΒΆ
- NORMALIZED = 'normalized'ΒΆ
- property function: Callable[[DedupMailMixin], str]ΒΆ
The callable producing this memberβs body hash for one mail.
- class mail_deduplicate.deduplicate.DuplicateSet(hash_key, mail_set, conf)[source]ΒΆ
Bases:
objectA set of mails sharing the same hash.
Implements all the safety checks required before we can apply any selection strategy.
Load-up the duplicate set of mail and freeze pool.
Once loaded-up, the pool of parsed mails is considered frozen for the rest of the duplicate setβs life. This allows aggressive caching of lazy instance attributes depending on the pool content.
- selection: set[DedupMailMixin]ΒΆ
Mails selected after application of selection strategy.
- discard: set[DedupMailMixin]ΒΆ
Mails discarded after application of selection strategy.
- confΒΆ
Configuration shared from the main deduplication process.
- pool: frozenset[DedupMailMixin]ΒΆ
Pool referencing all duplicated mails and their attributes.
- property timestamps: tuple[float, ...][source]ΒΆ
Returns the timestamps of all mails in the set.
Raises
MissingTimestampsif a timestamp could not be derived for some mails, naming them so users can locate and fix them. See: https://github.com/kdeldycke/mail-deduplicate/issues/132
- check_differences()[source]ΒΆ
Checks all mails of the set against each other, for size and content differences within the limits imposed by the thresholds.
Instead of rejecting the whole set on the first offending pair, the mails involved in the most offending pairs are greedily set aside until every remaining pair passes the thresholds. This keeps a single outlier from preventing the deduplication of the true copies sharing its set. See: https://github.com/kdeldycke/mail-deduplicate/issues/851
Returns the mails to set aside, empty if the whole pool already passes.
Raises
SizeDiffAboveThresholdorContentDiffAboveThresholdif fewer than 2 mails would remain, in which case there is no coherent core of duplicates and the whole set is to be skipped, as before.- Return type:
- diff(mail_a, mail_b)[source]ΒΆ
Return difference in bytes between two mailsβ normalized body.
Todo
Rewrite the diff algorithm to not rely on naive unified diff result parsing.
- Return type:
- pretty_diff(mail_a, mail_b)[source]ΒΆ
Returns a verbose unified diff between two mailsβ normalized body.
- Return type:
- select()[source]ΒΆ
Settle which mails of the set are selected and which are discarded.
Run preliminary checks, then apply the strategies to the pool of mails, each in turn until one produces a proper selection.
The process results in two subsets of mails: the selected and the discarded.
- Return type:
- class mail_deduplicate.deduplicate.HashedMail(mail_hash: str | None, timestamp: float | None, mail_size: int | None, rejection: str | None)[source]ΒΆ
Bases:
NamedTupleWhat a hashing worker sends back for one mail.
Deliberately nothing but scalars: the parsed message stays in the worker and dies with the task, so a few dozen bytes cross the process boundary instead of the whole mail. Which mail an answer belongs to is not carried either, as
fan_out()hands every answer back alongside the task it came from. The parent rebuilds the same dehydrated stub it would have produced itself, exactly as it does for a mail restored from the cache.Create new instance of HashedMail(mail_hash, timestamp, mail_size, rejection)
- class mail_deduplicate.deduplicate.MailMeta(source_path: str, mail_id: str, path: str, timestamp: float | None, mail_size: int | None)[source]ΒΆ
Bases:
NamedTupleAll a worker needs to rebuild one mail of a duplicate set from its own file.
Create new instance of MailMeta(source_path, mail_id, path, timestamp, mail_size)
- class mail_deduplicate.deduplicate.SelectedSet(selected: tuple[tuple[str, str], ...], discarded: tuple[tuple[str, str], ...], stats: dict[Stat, int], records: tuple[tuple[int, str], ...])[source]ΒΆ
Bases:
NamedTupleWhat a selection worker sends back for one duplicate set.
Mails are named rather than returned: the parent already holds them, and shipping them back would mean pickling every message it worked so hard not to keep.
Create new instance of SelectedSet(selected, discarded, stats, records)
- class mail_deduplicate.deduplicate.Deduplicate(conf)[source]ΒΆ
Bases:
objectLoad-up messages, search for duplicates, apply selection strategy and perform the action.
Similar messages sharing the same hash are grouped together in a
DuplicateSet.- CHUNK_SIZE: int = 200ΒΆ
Mails handed to a hashing worker in one go.
Each queue round-trip costs far more than hashing a single mail, so tasks travel in chunks. Large enough to make that cost disappear, small enough that the last worker to finish does not hold up the others.
- SET_CHUNK_SIZE: int = 32ΒΆ
Duplicate sets handed to a selection worker in one go.
Smaller than the hashing chunk because a set is several mailsβ worth of work, so fewer of them already amortize the same round-trip.
- sources: dict[str, Mailbox]ΒΆ
Index of mail sources by their full, normalized path. So we can refer to them in Mail instances. Also have the nice side effect of natural deduplication of sources themselves.
- mails: dict[str, list[DedupMailMixin]]ΒΆ
All mails grouped by hashes.
Grouped in lists rather than sets: mails carry no value equality, so a set only ever deduplicated by object identity, which the single pass over each box already rules out. A one-element list also costs 64 bytes where a set costs 216, and most hashes group a single mail.
- selection: set[DedupMailMixin]ΒΆ
Mails selected after application of selection strategy.
- discard: set[DedupMailMixin]ΒΆ
Mails discarded after application of selection strategy.
- link_targets: dict[DedupMailMixin, DedupMailMixin]ΒΆ
Maps each discarded mail to the selected mail it can be hardlinked to.
Left empty unless the configured action consumes it: see
track_link_targets.
- confΒΆ
Configuration shared across the deduplication process.
- track_link_targets: boolΒΆ
Whether each discarded mail has to be paired with a selected one.
Only the hardlinking action needs that pairing, and it costs one dictionary entry per discarded mail, so it is settled once here and only recorded when something reads it.
- cache: HashCache | NoneΒΆ
Cross-run cache of mail hashes, when the user opted in with
--cacheand the database could be opened.
- restore_cached(box, mail_id, entry)[source]ΒΆ
Rebuild a hashed mail from its cache entry, without opening its file.
Produces the same dehydrated stub the hashing step would have left behind, with the scalars the later steps rely on already memoized. Anything else those steps need is re-read from the box on demand, as for any other mail.
- Return type:
- blank_stub(box, mail_id)[source]ΒΆ
An empty mail carrying only its identity, ready to be filled in.
Stands in for a mail this process never parsed, because its content came from the cache or from a worker that has already thrown it away.
- Return type:
- parallel_boxes()[source]ΒΆ
The sources whose mails a worker process could hash on its own.
Only folder-based boxes qualify. Their mails each own a file, which a worker opens by path, sharing nothing. Mails of a file-based box are byte ranges of one file that a single handle seeks through, so handing them out would mean several processes seeking the same descriptor.
Returns an empty list unless every source qualifies, so a run is either wholly parallel or wholly sequential rather than silently half of each.
- worker_pool(jobs, initializer, initargs, verb, doing)[source]ΒΆ
Hand out a pool of worker processes prepared by the initializer, and shut it down however the block it wraps ends.
Yields
Noneinstead when the pool cannot be started, which is the case in some frozen or sandboxed environments: the initializer is then run right here, so the worker functions find the state they expect, and the caller degrades to sequential calls instead of dying.- Return type:
- fan_out(pool, worker, tasks, window, chunksize)[source]ΒΆ
Yields each task back alongside its workerβs answer.
Tasks pair what the parent keeps with the payload its worker receives, and are handed out a window at a time rather than all at once:
map()consumes whatever it is given immediately, so passing the whole corpus would hold a task per mail, undoing the flat memory the rest of the run maintains. The window is wide enough that every worker always has chunks queued behind it.With or without a pool, answers come back in submission order, so whatever the caller aggregates comes out identical to a sequential run at any number of workers.
- uncached(boxes, absorb, progress)[source]ΒΆ
Yields the identity of every mail the cache cannot answer for.
A mail the cache can restore is absorbed here and never even opened, which is the whole point of keeping the cache. Lazy, and driven from the main process alone: neither box objects nor the cache are safe for concurrent access.
- hash_in_parallel(jobs, absorb, progress)[source]ΒΆ
Hash every uncached mail across a pool of worker processes.
Threads cannot do this job: what hashing spends itself on is Python-level work that the interpreter lock serializes, so fanning it out across threads only adds contention. Processes sidestep the lock, and the mail never has to travel: a worker opens its own file and sends back a hash and two scalars.
- Return type:
- adopt_hashed(box, mail_id, result)[source]ΒΆ
Turn a workerβs answer back into the mail stub this process works with.
- Return type:
tuple[DedupMailMixin,str|None,TooFewHeaders|None]
- add_source(source_path)[source]ΒΆ
Registers a source of mails, validates and opens it.
Duplicate sources of mails are not allowed, as when we perform the action, we use the path as a unique key to tie back a mail from its source.
- Return type:
- hash_all()[source]ΒΆ
Browse all mails from all registered sources, compute hashes and group mails by hash.
Displays a progress bar as the operation might be slow.
Each mail is dehydrated as soon as it is hashed, so whatever the size of the corpus, only a lightweight stub of every mail is retained. See: https://github.com/kdeldycke/mail-deduplicate/issues/761
Hashing fans out across worker processes when
--jobsresolves above 1 and every source is a folder-based box; otherwise mails stream through this process one at a time, which is also the lowest-memory path. Box listing and the hash cache always stay in this process, as neither is safe for concurrent access, so the speedup is largest where the hashing itself is the cost, with--hash-body raw/normalized.- Return type:
- build_sets()[source]ΒΆ
Build the selected and discarded sets from each duplicate set.
The selection is settled one duplicate set at a time, to keep the memory footprint low and make the log easier to read.
- Return type:
- settle(hash_key, mail_set)[source]ΒΆ
Run one duplicate set through the thresholds and the strategies, right in this process, and merge its verdict into the run.
- Return type:
- log_set_heading(hash_key, mail_count)[source]ΒΆ
Announce a duplicate set, at a level reflecting whether it holds copies.
Styling the heading is not free, and most sets hold a single mail and so report at debug level, where the result is thrown away: only pay for it when it is actually logged.
- Return type:
- release(mail_set)[source]ΒΆ
Drop the content the thresholds and strategies pulled back from the boxes.
Iterating on the original set covers skipped sets and set-aside mails too, which land in neither selection nor discard. See: https://github.com/kdeldycke/mail-deduplicate/issues/362
- Return type:
- record_link_targets(selection, discard)[source]ΒΆ
Pair every discarded mail of a set with a selected mail of that same set.
Hardlinking a discarded mail only makes sense against a copy that survives the very set both were found in, and that pairing is the one thing the flat selection and discard sets no longer say once every set has been settled.
A strategy is free to keep several mails, so the target is the one with the lowest path: a run then links to the same copy however the sets came back, which matters when the selection is spread over a pool of processes.
Iterables are only walked once the run is known to need them, so a call from a non-hardlinking run costs nothing beyond the arguments themselves.
- Return type:
- select_in_parallel(jobs, progress)[source]ΒΆ
Apply the selection to every duplicate set across a pool of processes.
Duplicate sets share nothing with one another, so a set is the natural unit of work. Only sets holding copies are handed out: a set of one is settled without reading anything, and would cost more to ship than to decide.
- Return type:
- describe(mail)[source]ΒΆ
Everything a worker needs to rebuild a mail, and nothing more.
- Return type:
- adopt_selection(hash_key, mail_set, result)[source]ΒΆ
Merge a workerβs verdict on one set back into this process.
The heading and the workerβs own messages are said here, in that order, so the log reads as it would have from a sequential run however the sets were spread out.
- Return type:
mail_deduplicate.mail moduleΒΆ
A mail wrapped with the deduplication-specific properties: canonical hash, normalized headers, timestamp, size, and the dehydration machinery keeping memory flat.
- exception mail_deduplicate.mail.TooFewHeaders[source]ΒΆ
Bases:
ExceptionNot enough headers were found to produce a solid hash.
- class mail_deduplicate.mail.TimeSource(*values)[source]ΒΆ
Bases:
StrEnumEnumeration of all supported mail timestamp sources.
- DATE_HEADER = 'date-header'ΒΆ
Timestamp sourced from the messageβs
Dateheader.
- CTIME = 'ctime'ΒΆ
Timestamp is from the emailβs file on the filesystem.
Attention
Only meaningful for sources storing one mail per file, like
maildirandeml.
- mail_deduplicate.mail.ADDRESS_HEADERS = frozenset({'bcc', 'cc', 'delivered-to', 'disposition-notification-to', 'envelope-to', 'from', 'original-recipient', 'reply-to', 'resent-bcc', 'resent-cc', 'resent-from', 'resent-reply-to', 'resent-sender', 'resent-to', 'return-path', 'sender', 'to', 'x-envelope-from', 'x-envelope-to', 'x-original-to'})ΒΆ
Headers that contain email addresses.
Hint
Headers from which quotes should be discarded, so
"Bob" <bob@example.com>hashes to the same thing asBob <bob@example.com>.Attention
These IDs should be kept lower-case, because they are compared to the IDs provided to the
-h/--hash-headeroption, carried by thehash_headersentry of the configuration.
- class mail_deduplicate.mail.DedupMailMixin(message=None)[source]ΒΆ
Bases:
MessageMessage with deduplication-specific properties and utilities.
Extends standard libraryβs mailbox.Message, and shouldnβt be used directly, but composed with
mailbox.Messagesub-classes.Initialize a Message instance.
- CONTENT_CACHES: tuple[str, ...] = ('body_lines', 'canonical_headers', 'hash_raw_body', 'hash_normalized_body')ΒΆ
Memoized properties derived from the message content.
They are only needed while computing hashes, and are dropped by
dehydrate()alongside the parsed message itself.
- resolve_path: Callable[[Mailbox, str], str]ΒΆ
Derives the real filesystem location of a mail from its box.
Set per box format by
make_dedup_mail(), as folder-based boxes give each mail its own file while file-based ones pack them all into the boxβs single file.
- defects: list = ()ΒΆ
Fallback for the parsing defects dropped by
dehydrate().An empty tuple rather than a list, so it can be shared by every dehydrated mail instead of costing one throw-away empty list each: reads still work, and the appends only the parser performs fail loudly on a mail that has no message.
- PARSED_MESSAGE_ATTRS: tuple[str, ...] = ('_headers', '_payload', '_unixfrom', 'preamble', 'epilogue', 'defects')ΒΆ
Attributes carrying the parsed message, as populated by
email.parser.They hold the bulk of a mailβs memory footprint, and are the ones dropped by
dehydrate()and restored byhydrate().
- box: Mailbox | NoneΒΆ
The box this message was read from, kept to re-fetch its content on demand after dehydration.
- property path: strΒΆ
Real filesystem location of the mail.
Returns the individual mailβs file for folder-based box types (
maildir& co.), but returns the whole box path for file-based boxes (mbox& co.). Used by regexp-based selection strategies and to render the mailβs repr.Derived on access from the box rather than stored, so a retained mail does not carry a copy of its own absolute path for the whole run. Raises
AttributeErrorbefore the box metadata is attached, so getattr(mail, βpathβ, None) still reads as absent.
- add_box_metadata(box, mail_id)[source]ΒΆ
Post-instantiation utility to attach to mail some metadata derived from its parent box.
Called right after the
__init__()constructor.This allows the mail to carry its own information on its origin box and index.
- Return type:
- dehydrate()[source]ΒΆ
Reduce the mail to the lightweight metadata needed by the next steps.
Memoizes the scalar properties consumed by selection strategies (timestamp, and size when the decoded body is at hand), then drops the parsed message and every cached copy of its content. This cuts the resident footprint of a retained mail from the full size of its message to a few hundred bytes, whatever the number of mails processed. See: https://github.com/kdeldycke/mail-deduplicate/issues/761
No-op if the mail is already dehydrated. The dropped content is re-read from the source box by
hydrate()when a later step needs it again.- Return type:
- hydrate()[source]ΒΆ
Restore the full parsed message dropped by
dehydrate().Re-reads the mail from its source box, through the same parsing path that produced it in the first place, so the restored content is identical. No-op if the mail still carries its parsed message.
- Return type:
- hydrated()[source]ΒΆ
Borrow the full parsed message for the duration of the block.
Restores the message on the way in and releases it on the way out, so a step needing the whole mail after the hashing one keeps its content resident only while it uses it, and memory stays flat across a loop of mails.
- Return type:
- property parsed_date: float | None[source]ΒΆ
Parse the mailβs date header into float timestamp.
Returns
Noneif the mail has no valid date header.Self-hydrating: re-reads the message from its box if it was dehydrated.
- property timestamp: float | None[source]ΒΆ
Compute the normalized canonical timestamp of the mail.
Sourced from the messageβs
Dateheader by default. In the case ofmaildir, can be sourced from the emailβs file from the filesystem.Warning
ctimedoes not refer to creation time on POSIX systems, but rather the time of the last metadata change.Todo
Investigate what mailbox.MaildirMessage.get_date() does and if we can use it.
- property size: int[source]ΒΆ
Returns canonical mail size.
Size is computed as the length of the message body, i.e. the payload of the mail stripped of all its headers, not from the mail file persisting on the file- system.
Todo
Allow customization of the way the size is computed, by getting the file size instead, for example with
os.path.getsize(mail_file).
- property body_lines: list[str][source]ΒΆ
Return a normalized list of lines from messageβs body.
Self-hydrating: re-reads the message from its box if it was dehydrated.
- hash_key()[source]ΒΆ
Returns the canonical hash of a mail.
Caution
This method hasnβt been made explicitly into a cached property in order to reduce the overall memory footprint.
- Return type:
- property canonical_headers: tuple[tuple[str, str], ...][source]ΒΆ
Returns the full list of all canonical headers names and values in preparation for hashing.
Self-hydrating: re-reads the message from its box if it was dehydrated.
- pretty_canonical_headers()[source]ΒΆ
Renders a table of headers names and values used to produce the mailβs hash.
Caution
This method hasnβt been explicitly made into a cached property in order to reduce the overall memory footprint.
Returns a string ready to be printed.
- Return type:
- serialized_headers()[source]ΒΆ
Serialize the canonical headers into a single string ready to be hashed.
At this point we should have an absolute minimum of headers.
Caution
This method hasnβt been explicitly made into a cached property in order to reduce the overall memory footprint.
- Return type:
- normalized_header_values(header_id)[source]ΒΆ
Returns all normalized values of a header.
Values are cleaned-up into their canonical form.
- normalize_subject(subject)[source]ΒΆ
Strip
Re:/Fwd:and[list-name]prefixes fromSubject.This cleans up prefixes automatically added by mailing list software, since the mail could have been
CCβd to multiple lists, in which case it will receive a different prefix for each.- Return type:
- normalize_content_type(value)[source]ΒΆ
Normalize
Content-Typeby stripping parameters.Removes everything after the semicolon, keeping only the MIME type. E.g.,
text/plain; charset=utf-8becomestext/plain.Apparently list servers actually munge
Content-Typee.g. by stripping the quotes fromcharset="us-ascii". Section 5.1 of RFC2045 says that either form is valid (and they are equivalent).Additionally, with multipart/mixed, boundary delimiters can vary by recipient. We need to allow for duplicates coming from multiple recipients, since for example you could be signed up to the same list twice with different addresses. Or maybe someone bounces you a load of mail some of which is from a mailing list youβre both subscribed to - then itβs still useful to be able to eliminate duplicates.
- Return type:
- normalize_date(value)[source]ΒΆ
Normalize
DatetoYYYY-MM-DDformat.Date timestamps can differ by seconds or hours for various reasons, so letβs only honour the date for now and normalize them to UTC timezone.
- Return type:
- normalize_address_header(value)[source]ΒΆ
Normalize address headers by removing quotes and collapsing whitespace.
E.g.,
"Bob" <bob@example.com>becomesBob <bob@example.com>.Remove quotes in any headers that contain addresses to ensure a quoted name is hashed to the same value as an unquoted one.
Danger
This may not be the cleanest way to normalize email addresses. E.g.
"Robert \"Bob\"``becomesRobert \Bob\, but this shouldnβt matter for hashing purposes as weβre just trying to get a good heuristic. Refs: #847 and #846.- Return type:
- normalize_message_id(value)[source]ΒΆ
Normalize Message-ID header by stripping angle brackets.
E.g.,
<unique-id@example.com>becomesunique-id@example.com.- Return type:
- strip_angle_brackets(value)[source]ΒΆ
Strip angle brackets from a value if itβs a single bracketed item.
Only strips if the value matches
<something>with no commas.Note
Sometimes
email.parserstrips the<>brackets from aTo:header which has a single address. I have seen this happen for only one mail in a duplicate pair. Iβm not sure why (presumably the parser usesemail.utils.unquotesomewhere in its code path which was only triggered by that mail and not its sister mail), but to be safe, we should always strip the<>brackets to avoid this difference preventing duplicate detection.- Return type:
mail_deduplicate.mail_box moduleΒΆ
Utilities to read and write mail boxes in various formats.
Based on Pythonβs standard library mailbox module.
- mail_deduplicate.mail_box.maildir_mail_path(box, key)[source]ΒΆ
Location of a
maildirmail, read from the boxβs table of contents.A maildir key drops the
:2,<flags>suffix the file name carries, so the name cannot be rebuilt from the key alone. The box refreshed its table of contents to hand out the mail in the first place, so it is only re-validated on a miss.- Return type:
- mail_deduplicate.mail_box.keyed_mail_path(box, key)[source]ΒΆ
Location of an
MHoremlmail, whose file is named after its key.- Return type:
- mail_deduplicate.mail_box.box_file_path(box, key)[source]ΒΆ
Location of a mail from a file-based box: the boxβs own single file.
Every mail of an
mbox,babylormmdfbox is packed into it, so they all share this path and are told apart by their mail ID.- Return type:
- mail_deduplicate.mail_box.iter_mail_ids(box)[source]ΒΆ
Yields the key of every mail held by a box.
Maildir.iterkeys()confirms that each key still resolves to a file, onestatper mail, on top of the directory listing it has just built. Every caller here goes on to stat or open that same file anyway, and copes with its disappearance, so the check is paid for twice and needed once: reading the refreshed table of contents directly skips it.The other formats list their mails without that extra round, and are left to their own iterator.
- mail_deduplicate.mail_box.resolve_mail_path(box, key)[source]ΒΆ
Location of a mail in its box, without instantiating the mail.
Lets a caller reach a mailβs file before deciding to read it, which is how the hash cache checks whether a mail changed without paying for its parsing.
- Return type:
- mail_deduplicate.mail_box.make_dedup_mail(name, base, path_resolver)[source]ΒΆ
Create a DedupMail class for a mailbox message type.
Deriving a mailβs own location from its box is format-specific, so the resolver is baked into the class here instead of being branched on at runtime.
- Return type:
- class mail_deduplicate.mail_box.MaildirDedupMail(message=None)ΒΆ
Bases:
DedupMailMixin,MaildirMessageInitialize a MaildirMessage instance.
- static resolve_path(key)ΒΆ
Location of a
maildirmail, read from the boxβs table of contents.A maildir key drops the
:2,<flags>suffix the file name carries, so the name cannot be rebuilt from the key alone. The box refreshed its table of contents to hand out the mail in the first place, so it is only re-validated on a miss.- Return type:
- class mail_deduplicate.mail_box.mboxDedupMail(message=None)ΒΆ
Bases:
DedupMailMixin,mboxMessageInitialize an mboxMMDFMessage instance.
- class mail_deduplicate.mail_box.MHDedupMail(message=None)ΒΆ
Bases:
DedupMailMixin,MHMessageInitialize an MHMessage instance.
- class mail_deduplicate.mail_box.BabylDedupMail(message=None)ΒΆ
Bases:
DedupMailMixin,BabylMessageInitialize a BabylMessage instance.
- class mail_deduplicate.mail_box.MMDFDedupMail(message=None)ΒΆ
Bases:
DedupMailMixin,MMDFMessageInitialize an mboxMMDFMessage instance.
- class mail_deduplicate.mail_box.EMLDedupMail(message=None)ΒΆ
Bases:
DedupMailMixin,MessageInitialize a Message instance.
- class mail_deduplicate.mail_box.EML(dirname, factory=None, create=True)[source]ΒΆ
Bases:
MailboxA folder of loose
.emlfiles, walked recursively.Supports mail archives exported as individual RFC 5322 files, one mail per file, as produced by Outlook PST/OST conversion tools for instance. See: https://github.com/kdeldycke/mail-deduplicate/issues/760
Keys are the paths of the mail files, relative to the folderβs root. Files without the
.emlextension (case-insensitive) are ignored, as well as hidden files and directories.Follows the interface of Pythonβs mailbox.Mailbox. Like
maildir, the one-file-per-mail storage needs no locking.Initialize a Mailbox instance.
- class mail_deduplicate.mail_box.BoxStructure(*values)[source]ΒΆ
Bases:
EnumBox structures can be file-based or folder-based.
- FOLDER = 1ΒΆ
- FILE = 2ΒΆ
- class mail_deduplicate.mail_box.BoxFormat(base_class, structure, message_class)[source]ΒΆ
Bases:
EnumIDs of all the supported box formats and their metadata.
Each entry is associated to:
their original base class,
the structure they implement (file-based or folder-based),
the custom message factory class to use.
From these, we can derive the proper constructor with our own custom
DedupMailfactory.Hint
This could be extended in the future to add support for other mailbox formats and sources, like Gmail accounts, IMAP servers, etc.
- MAILDIR = (<class 'mailbox.Maildir'>, BoxStructure.FOLDER, <class 'mail_deduplicate.mail_box.MaildirDedupMail'>)ΒΆ
- MBOX = (<class 'mailbox.mbox'>, BoxStructure.FILE, <class 'mail_deduplicate.mail_box.mboxDedupMail'>)ΒΆ
- MH = (<class 'mailbox.MH'>, BoxStructure.FOLDER, <class 'mail_deduplicate.mail_box.MHDedupMail'>)ΒΆ
- BABYL = (<class 'mailbox.Babyl'>, BoxStructure.FILE, <class 'mail_deduplicate.mail_box.BabylDedupMail'>)ΒΆ
- MMDF = (<class 'mailbox.MMDF'>, BoxStructure.FILE, <class 'mail_deduplicate.mail_box.MMDFDedupMail'>)ΒΆ
- EML = (<class 'mail_deduplicate.mail_box.EML'>, BoxStructure.FOLDER, <class 'mail_deduplicate.mail_box.EMLDedupMail'>)ΒΆ
- property constructorΒΆ
Return a constructor for this box format with our custom message factory.
- mail_deduplicate.mail_box.FOLDER_FORMATS = (BoxFormat.MAILDIR, BoxFormat.MH, BoxFormat.EML)ΒΆ
Box formats implementing a folder-based structure.
Is a tuple to keep natural order defined by
BoxFormat.
- mail_deduplicate.mail_box.FILE_FORMATS = (BoxFormat.MBOX, BoxFormat.BABYL, BoxFormat.MMDF)ΒΆ
Box formats implementing a file-based structure.
Is a tuple to keep natural order defined by
BoxFormat.
- mail_deduplicate.mail_box.FOLDER_FORMAT_CLASSES = (<class 'mailbox.Maildir'>, <class 'mailbox.MH'>, <class 'mail_deduplicate.mail_box.EML'>)ΒΆ
Base classes of folder-based box formats, as a tuple ready for
isinstance.
- mail_deduplicate.mail_box.MAILDIR_SUBDIRS = frozenset({'cur', 'new', 'tmp'})ΒΆ
List of required sub-folders defining a properly structured maildir.
- mail_deduplicate.mail_box.is_maildir(path)[source]ΒΆ
Returns
Truewhen the path holds all the sub-directories of a properly structured maildir.- Return type:
- mail_deduplicate.mail_box.contains_maildir(path)[source]ΒΆ
Returns
Truewhen the path is a maildir or holds one at any depth.Allows the discovery of nested maildir folders stored as plain directories, as produced by isync/mbsyncβs Verbatim naming style. See: https://github.com/kdeldycke/mail-deduplicate/issues/973
Dot-prefixed directories are ignored, as they are covered by the
Maildir++folder convention. The mail-holding sub-directories of maildirs are not explored either.- Return type:
- mail_deduplicate.mail_box.contains_eml(path)[source]ΒΆ
Returns
Truewhen the path holds at least one.emlfile, at any depth.Hidden files and directories are ignored, and the extension is matched case-insensitively, mirroring the walk of
EMLboxes.- Return type:
- mail_deduplicate.mail_box.autodetect_box_type(path)[source]ΒΆ
Auto-detect the format of the mailbox located at the provided path.
If the path is a file, then it is considered as an
mbox. Else, if the provided path is a folder and features theMAILDIR_SUBDIRSsub-directories, or holds nested maildir folders at any depth, it is parsed as amaildir. A folder holding loose.emlfiles instead is parsed as anemlsource.Todo
Future finer autodetection heuristics should be implemented here. Some ideas:
single mail from a
maildirplain text mail content
other mailbox formats supported in Pythonβs standard library:
MHBabylMMDF
- Return type:
- mail_deduplicate.mail_box.open_box(path, box_format=None, force_unlock=False)[source]ΒΆ
Open a mail box.
Returns a list of boxes, one per sub-folder. All are locked, ready for operations.
If
box_formatis provided, forces the opening of the box in the specified format. Else, defaults to autodetection.
- mail_deduplicate.mail_box.lock_box(box, force_unlock)[source]ΒΆ
Lock an opened box and allows for forced unlocking.
Returns the locked box.
- Return type:
- mail_deduplicate.mail_box.open_subfolders(box, force_unlock)[source]ΒΆ
Browse recursively the subfolder tree of a box.
Returns a list of opened and locked boxes, each for one subfolder.
Skips box types not supporting subfolders. For
maildir, both theMaildir++convention (dot-prefixed folders) and Verbatim-style layouts (nested plain directories, each a maildir of its own) are browsed. A directory without the maildir structure only acts as a container of nested folders and carries no mail of its own.
mail_deduplicate.strategy moduleΒΆ
Strategy definitions.
- mail_deduplicate.strategy.log_selection(message_template)[source]ΒΆ
Decorator to log selection criteria.
- mail_deduplicate.strategy.select_older(duplicates)[source]ΒΆ
Select all older duplicates.
Discards the newests, i.e. the subset sharing the most recent timestamp.
- Return type:
- mail_deduplicate.strategy.select_oldest(duplicates)[source]ΒΆ
Select all the oldest duplicates.
Discards the newers, i.e. all mail of the duplicate set but those sharing the oldest timestamp.
- Return type:
- mail_deduplicate.strategy.select_newer(duplicates)[source]ΒΆ
Select all newer duplicates.
Discards the oldest, i.e. the subset sharing the most ancient timestamp.
- Return type:
- mail_deduplicate.strategy.select_newest(duplicates)[source]ΒΆ
Select all the newest duplicates.
Discards the olders, i.e. all mail of the duplicate set but those sharing the newest timestamp.
- Return type:
- mail_deduplicate.strategy.select_smaller(duplicates)[source]ΒΆ
Select all smaller duplicates.
Discards the biggests, i.e. the subset sharing the biggest size.
- Return type:
- mail_deduplicate.strategy.select_smallest(duplicates)[source]ΒΆ
Select all the smallest duplicates.
Discards the biggers. i.e. all mail of the duplicate set but those sharing the smallest size.
- Return type:
- mail_deduplicate.strategy.select_bigger(duplicates)[source]ΒΆ
Select all bigger duplicates.
Discards the smallests, i.e. the subset sharing the smallest size.
- Return type:
- mail_deduplicate.strategy.select_biggest(duplicates)[source]ΒΆ
Select all the biggest duplicates.
Discards the smallers, i.e. all mail of the duplicate set but those sharing the biggest size.
- Return type:
- mail_deduplicate.strategy.select_matching_path(duplicates)[source]ΒΆ
Select all duplicates whose file path match the regular expression provided via the βregexp parameter.
- Return type:
- mail_deduplicate.strategy.select_non_matching_path(duplicates)[source]ΒΆ
Select all duplicates whose file path doesnβt match the regular expression provided via the βregexp parameter.
- Return type:
- mail_deduplicate.strategy.select_one(duplicates)[source]ΒΆ
Randomly select one duplicate, and discards all others.
- Return type:
- mail_deduplicate.strategy.select_all_but_one(duplicates)[source]ΒΆ
Randomly discard one duplicate, and select all others.
- Return type:
- class mail_deduplicate.strategy.Strategy(*values)[source]ΒΆ
Bases:
EnumSelection strategies to apply on a set of duplicate mails.
Each strategy in the
Enumpoints to the function implementing the selection logic, by way of thefunctionproperty.Strategies whose member value is a string are simply aliases to other strategies, pointing to the name of the function implementing the logic. The other members have integer values, to indicate their function ID is to be derived from the member name. This arrangement lets each member exist on its own instead of being hidden by the aliasing mechanism of
Enum.Aliases are great usability features to represent inverse operations. They help users reason about the selection operators in whichever direction matches their mental model.
- SELECT_OLDER = 1ΒΆ
- SELECT_OLDEST = 2ΒΆ
- SELECT_NEWER = 3ΒΆ
- SELECT_NEWEST = 4ΒΆ
- DISCARD_NEWEST = 'select_older'ΒΆ
- DISCARD_NEWER = 'select_oldest'ΒΆ
- DISCARD_OLDEST = 'select_newer'ΒΆ
- DISCARD_OLDER = 'select_newest'ΒΆ
- SELECT_SMALLER = 5ΒΆ
- SELECT_SMALLEST = 6ΒΆ
- SELECT_BIGGER = 7ΒΆ
- SELECT_BIGGEST = 8ΒΆ
- DISCARD_BIGGEST = 'select_smaller'ΒΆ
- DISCARD_BIGGER = 'select_smallest'ΒΆ
- DISCARD_SMALLEST = 'select_bigger'ΒΆ
- DISCARD_SMALLER = 'select_biggest'ΒΆ
- SELECT_MATCHING_PATH = 9ΒΆ
- SELECT_NON_MATCHING_PATH = 10ΒΆ
- DISCARD_NON_MATCHING_PATH = 'select_matching_path'ΒΆ
- DISCARD_MATCHING_PATH = 'select_non_matching_path'ΒΆ
- SELECT_ONE = 11ΒΆ
- SELECT_ALL_BUT_ONE = 12ΒΆ
- DISCARD_ALL_BUT_ONE = 'select_one'ΒΆ
- DISCARD_ONE = 'select_all_but_one'ΒΆ
- property function: Callable[[DuplicateSet], set[DedupMailMixin]]ΒΆ
The selection function this member stands for.
Alias members carry the functionβs name as their value; canonical members derive it from their own name.