Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: CLI commands for database management #2654

Merged

Conversation

polarathene
Copy link
Member

@polarathene polarathene commented Jun 21, 2022

Description

In preparation for switching the file locking back to flock, I wanted to clean up the CLI commands involved, but got a bit carried away 😅

It should also help with keeping the encryption feature PR simple to resolve/test.

Commands refactored:

  • User (All: add / list / update / del + dovecot-master variants)
  • Quota (All: set / del)
  • Virtual Alias (All: add / list /del)
  • Relay (All: add-relayhost / add-sasl / exclude-domain)

Overall changes involve:

  • Fairly common structure:
    • _main method at the top provides an overview of logical steps:
      • After all methods are declared beneath it (and imported from the new helpers/database/db.sh), the _main is called at the bottom of the file.
      • delmailuser additionally processes option support for -y prior to calling _main.
    • __usage is now consistent with each of these commands, along with the help command.
    • Most logic delegated to new helper scripts. Some duplicate content remains on the basis that it's low-risk to maintenance and avoids less hassle to jump between files to check a single line, usually this is arg validation.
    • Error handling should be more consistent, along with var names (no more USER/EMAIL/FULL_EMAIL to refer to the same expected value).
  • Three new management scripts (in helpers/database/manage/) using a common structure for managing changes to their respective "Database" config file.
    • postfix-accounts.sh unified not only add and update commands, but also all the dovecot-master versions, a single password call for all 4 of them, with a 5th consumer of the password prompt from the relay command addsaslpassword.
    • These scripts delegate actual writes to helpers/database/db.sh which provides a common API to support the changes made.
      • This is more verbose/complex vs the current inline operations each command currently has, as it provides generic support instead of slightly different variations being maintained, along with handling some edge cases that existed and would lead to bugs (notably substring matches).
      • Centralizing changes here seems wiser than scattered about. I've tried to make it easy to grok, hopefully it's not worse than the current situation.
      • List operations were kept in their respective commands, db.sh is only really managing writes. I didn't see a nice way for removing the code duplication for list commands as the duplication was fairly minimal, especially for listalias and listdovecotmasteruser which were quite simple in their differences in the loop body.
      • listmailuser and delmailuser also retain methods exclusive to respective commands, I wasn't sure if there was any benefit to move those, but they were refactored.

Happy to drop the helpers/database/** files if it seems a bit over-engineered 👍

I figured with flock the locks can often be done in the db.sh:_db_operation method, only some methods may want to lock multiple files for larger operations such as delmailuser. This would be fine with flock -x in the same process, and not conflict with flock -x in _db_operation (it'll re-use the existing lock for the FD it has instead of blocking).


Apologies for the large commit history not likely being that helpful for review (file diffs probably aren't great either). I went through several iterations to reach this point.

If the PR changes are a bit large for review, let me know and I can split this out into smaller PRs.

Commit messages for reference
* refactor: `addmailuser` + `adddovecotmasteruser`

These two are very similar, updated together.

Restructures logic into functions, additional comments, local vars.

* refactor: `updatemailuser` + `updatedovecotmasteruser`

These two are very similar, updated together.

Restructures logic into functions, additional comments, local vars.

* refactor: `delmailuser` + `deldovecotmasteruser`

These two are very similar, updated together.

Restructures logic into functions, additional comments, local vars.

* refactor: `listmailuser`

Restructures logic into functions, additional comments, local vars.

Heavier refactor for this file. Dovecot Quota logic refactored, listing account aliases logic wrapped into it's own method.

`echo` output adjusted to not depend on conditional branches, but could be reverted.

* refactor: `listalias` + `listdovecotmasteruser`

These two are very similar, updated together.

Restructures logic into functions, additional comments, local vars.

* refactor: `addalias`

Restructures logic into functions, additional comments, local vars.

* refactor: `delalias`

Restructures logic into functions, additional comments, local vars.

* refactor: `setquota` + `delquota`

These two are very similar, updated together.

Restructures logic into functions, additional comments, local vars.

* refactor: Relay commands

Restructures logic into functions, additional comments, local vars.

* chore: Normalize to `MAIL_ACCOUNT` var

* chore: Corrections for `addalias` + `delalias`

`MAIL_ACCOUNT` usage and method names for these files were misunderstood, renaming to what they should be.

* chore: Update `sed` pattern

Should not use white-space as a delimiter in the pattern..

* chore: Use `_main` to position main logic at the top

* chore: `shellcheck` rule SC2904 no longer needed

* chore: Revise common method for relay commands

* chore: Sync common code `addmailuser` + `adddovecotmasteruser`

Majority of shared logic extracted into `add-account.sh` for maintenance.

* chore: Sync common code `updatemailuser` + `updatedovecotmasteruser`

Majority of shared logic extracted into `update-account.sh` for maintenance.

* chore: Align `addsaslpassword` with other password prompt methods

* chore: Share common helper `_if_missing_request_password`

* chore: Add helper `_hash_password`

* chore: Add helper `_account_already_exists`

* chore: Handle`_account_already_exists` errors in helper

* chore: Sync `MAIL_ACCOUNT` validation into helper method

* chore: Migrate dovecot quota methods to a common helper

* chore: Minor revisions

- Added some notes.
- Renamed some methods, `_password_hash` now takes args instead of using external vars.

* chore Minor revisions part 2

Bring back a common `_validate_paramters()` to command files for now.

Have `_account_already_exists` default to `postfix-accounts.cf` as that's the most commonly used DB for this method.

Renamed `_mail_account` helper methods to scope name to the context.

* chore: Use single helper file for accounts

* chore: Move common method for relay commands to helper method

* refactor: Migrate virtual alias methods into their own helper

- Logic restructured heavily in `listmailuser`, also fixing the quota regression I introduced (when moving the `ENABLE_QUOTA` guard into `_quota_show_for()`).
- `addalias` + `delalias` share common validation check in the new helper, their migrated methods is unaltered.
- The new helper adds some extra documentation.

* refactor: `listalias` + `listmailuser` + `listdovecotmasteruser`

Adds a common helper method to perform the DB check and list iterations while delegating a method for each to format the output per line/entry as they see fit.

This helper calls an internal method that would be defined earlier by each command. Similar to how `__usage` is called by several methods in `helper.sh` already.

* chore: `delmailuser` revisions

* refactor: `delmailuser` + `deldovecotmasteruser`

Methods updated to use explicit args instead of implicit ones. Bit easier to reason with currently despite the added verbosity/noise.

Added a `_key_exists_in_db` method to `helper.sh`, links common `grep -qi` lines that perform this lookup.

---

Refactor `delmailuser` DB entry removals to delegate to removal methods.  Rearranged the order, added inline docs explain why account removal occurs last, also moved maildir removal into this method.

`_alias_remove_for_recipient` is roughly the same logic as what `delmailuser` had for account removal from `postfix-virtual.cf`. Prefer this method of the version in `delmailuser`, but:
- Adopt the escaped inputs, so `.` only matches `.`, not _any_ character.
- Change white-space match of first expression to white-space token `\s` and expect one or more (`+`), not zero or more (`*`) to ensure there is a white-space separator.

Changed quota removal section to output a failure error like the other two removals do. Updated the quota helper method to accommodate by returning the status of sed (via sedfile) and not failing if no account exists to remove.

Account removal logic is shared/duplicated between `delmailuser` and `deldovecotmasteruser`. In `delmailuser` there is extra notes about account checking and info log that should perhaps be addressed.

Addressed the exit concern for `MAILDEL` handling in `_remove_maildir`, return seems to be the original intention.

* chore: Update `__usage` for each command

For the commands tackled by this PR, the `__usage` output has been normalized for consistency:
- Revisions to existing content such as in `DESCRIPTION` and `EXAMPLES`, renaming `SYNOPSIS` to `USAGE` and removal of `NAME` section.
- The command name at the top of the output is now lower-case.

`delmailuser` dropped `-h` option and `USAGE` syntax that accommodated the `help` option/sub-command.

`deldovecotmasteruser` has no options to parse, removed the OPTIND shift.

* fix: `setquota` and `delmailuser` (alias removal)

The original `setquota` prompt didn't actually prompt (`-p`), but used `-s` which silents the input, meant for passwords..

Added extra comments to clarify Dovecot quota value retrieved is in units of kibibytes, which is what we multiply by to get the total number of bytes for converting to IEC unit.

---

In `delmailuser`, the shared alias removal code I had it used did not have parity with the full entry removal pattern as a blank alias key was required (due to `^`), corrected by falling back to pattern matching non-whitespace (the alias key) for when the alias arg is empty.

* refactor: DB methods

`_key_exists_in_db` and `_db_add_or_replace_entry` now handle escaping of args internally. Relay commands were not escaping for regex pattern usage previously.

`_db_add_or_replace_entry` now takes an additional DELIMITER arg so that it can be used elsewhere (`setquota`).

`DELIMITER` is escaped for regex lookup if it's `|` (used for mail account delimiter) due to using extended regex. May consider reverting need for extended regex.

`_quota_exists_for_account` no longer needed.

* refactor: DB operations

- Manage DB delimiters in the main helper instead.
- Modified DB operations to several methods that forward args to single method using case statements with an action/operation to perform (reducing verbosity from duplicate operation code). This now additionally supports entry appending and removing.
- Updated helpers to use this new method instead (dropping redundant code), and existing ones to adapt to new args.
- Reverted extended regexp syntax, probably not relevant here? (ideally we forbid such special chars prior)

* refactor: DB operations alternative approach

- Changed back to separate KEY and VALUE args, removal operations would not have a delimiter to split from the ENTRY value otherwise.
- Fixed append operation for virtual-alias, missing the `,` prefix in it's VALUE.
- DATABASE moved to first arg to support remove operation better (avoids need to transpose args).
- Shortened db convenience method names, removing redundant suffix.
- This change removed need for the unpleasant ENTRY delimiter splitting for deriving KEY and VALUE. In exchange when composing ENTRY value we need to replace the DELIMITER for writing when it is `\s`.

* refactor: DB 'remove' + 'append' to support `postfix-virtual.cf`

The virtual alias DB handling has been moved into the helper for generic DB operations.

The logic was not ensuring exact matches for VALUE removal and appending (already exists check) leading to false-positives.

`_alias_list_for_account` still needs revision for proper exact matches.

* refactor: Unify account management

Create/Update/Delete methods can all leverage the same helper method, with convenience wrappers to simplify command API.

This seems better, commands have now inlined some methods, and may inline the parameter validation at a later point.

All operations are now using a lock from a single place, but this may better suit being moved into the main DB operation method, with the only concern being `delmailuser` may want to lock several files it touches? (regardless of loop concern).

* refactor: Unify quota management

Maybe not as useful for this helper due to only two methods and three consumers (setquota, delquota, delmailuser).

Methods used exclusively for `listmailuser` and `setquota` have been returned to those commands.

* refactor: Unify virtual alias management

Similar to previous commit for quota, the two methods with three consumers (addalias, delalias, delmailuser) adopt the same switch case approach.

Validation of args inlined. Args have the same order again, no longer supporting blank `MAIL_ALIAS`, instead expecting `_` to explicitly set the scope to all aliases.

`listmailuser` specific method returned to the command.

* chore: `_create_lock` is not reliable

In it's present form this only locks the command script itself, not a target file (Database) from other commands.

Kept it here as a reminder. It would be more appropriate now in `_db_operation`, however the current locking support would require more intricate lock releasing in that method, and would not be as ideal as locking the files prior to the loop starting..

* chore: Shift account validation into accounts helper

* chore: Return list iteration to commands

For similar reasons to allowing some duplicate code with relay commands, the list iterator was move into each list command to prefer readability.

Slightly improved the alias list conditional.

* chore: Minor revisions to relay commands

Validation shifted back.

The duplication is not as ideal for the common DOMAIN arg, but neither is having to reference it from a single file. 

MAIL_ACCOUNT and it's validation wasn't accurate message for the purpose of a relay account used as credential. Opted to inline as well.

* refactor: Minor revisions to `delmailuser`

Switch `MAILDEL` from boolean commands to 0 and 1 values, where 1 is true/enabled.

`MAILDEL` conditional message for `_remove_maildir` seemed redundant, especially in the context of a loop. May be of some use without loop support.

Folded mailbox directory check, anything after it assumes directory is available, so no conditional wrapping required.

* chore: Relocate DB helper scripts

* chore: Adjust imports

* chore: Minor revisions

- Imports did not work well for `db.sh`, mimic the `helpers/index.sh` approach instead.

- `_db_operation` had a typo with arg references.

- `_check_database_has_content` namespaced under `_db` prefix. Uses the `should` convention to convey expectation/validation intent. Similar was done for `_key_exists_in_db` which has different semantics (no failure built-in), thus avoids `should`. Both moved down to bottom of file as validation focused.

- `_db_get_delimiter_for` pushed further down in the file, added clarification it is an internal method now.

- `_password_hash` is only used in one location now, not 4, inlined it.

- Migrated `_password_request_if_missing` to `postfix-accounts.sh` as that's the main consumer, with only `addsaslpassword` also using it.

- Additional context comments on consumers of some methods.

* fix: DB operation corrections

Better distinguish between DELIMITER for KEY and VALUE (may differ for lists or writing value) with `K_` and `V_` prefixes.

Sync'd duplicate block in `_db_operation` and `_db_has_entry_with_key`.

---

`replace` for ENTRY was breaking for `updatemailuser` due to sed replacment var containing `/` (password base64 encoded), where previously the method had sed use ` ` (space) as a sed  substitution command delimiter.

`append` does not use the new `__escape_sed_replacement` method for `VALUE`, although perhaps it should for consistency?

---

Removing value from a list failed an alias test. There was no condition to match the left-most value in a list of multiple items.

Fixed by using an OR condition on the third expression, which it will collapse to the value captured (`\1`). This can be `\s` and the matched white-space value will be the captured value.

---

`__db_list_already_contains_value` doesn't seem to need `-n` + `p`? I cannot think of a scenario where the logic breaks now.

If it does, revert logic back, and add a third sed expression before grep that should output single values when no delimiter is present `-e "/^\S\+$/p"`. Previously single value items were not matched..

Revised the inline docs for the method.

* fix: Create config dir for databases if it doesn't exist

Only during an "add" fallback operation.

* tests: Make them pass

Current behaviour introduced was to error that the user does not exist, even if the database does not exist.

For now satisfying the test that expects a soft failure without an error.

* lint: Appease the lint gods with an offering of guidance

`source-path` added to `index.sh` is sufficient if used directly above the `source` line, otherwise it can be placed early on in the sourced file, but must be before the dependent `source` lines appear (function scope is ignored).

Likewise, I forgot that the `PATH_TO_SCRIPTS` var is only for a common ancestor of the real path and shellchecks source path, it's stripped/ignored by shellcheck in favor of trying any known source-path. So `/manage` must be added to each of these `source` lines instead.

The prior `source-path` applied within the function, as noted only applied to the direct line beneath it, not the entire scope.

Added comments for future maintainers to be aware of. Decided to keep it all within this `db.,sh` file, rather than `helpers/index.sh` , although perhaps that's a better place to document/demonstrate such. Otherwise `test/linting/lint.sh` covers this advice for the most part.

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Improvement (non-breaking change that does improve existing functionality)

Checklist:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • New and existing unit tests pass locally with my changes

These two are very similar, updated together.

Restructures logic into functions, additional comments, local vars.
These two are very similar, updated together.

Restructures logic into functions, additional comments, local vars.
These two are very similar, updated together.

Restructures logic into functions, additional comments, local vars.
Restructures logic into functions, additional comments, local vars.

Heavier refactor for this file. Dovecot Quota logic refactored, listing account aliases logic wrapped into it's own method.

`echo` output adjusted to not depend on conditional branches, but could be reverted.
These two are very similar, updated together.

Restructures logic into functions, additional comments, local vars.
Restructures logic into functions, additional comments, local vars.
Restructures logic into functions, additional comments, local vars.
These two are very similar, updated together.

Restructures logic into functions, additional comments, local vars.
Restructures logic into functions, additional comments, local vars.
`MAIL_ACCOUNT` usage and method names for these files were misunderstood, renaming to what they should be.
Should not use white-space as a delimiter in the pattern..
Majority of shared logic extracted into `add-account.sh` for maintenance.
Majority of shared logic extracted into `update-account.sh` for maintenance.
- Added some notes.
- Renamed some methods, `_password_hash` now takes args instead of using external vars.
Bring back a common `_validate_paramters()` to command files for now.

Have `_account_already_exists` default to `postfix-accounts.cf` as that's the most commonly used DB for this method.

Renamed `_mail_account` helper methods to scope name to the context.
- Logic restructured heavily in `listmailuser`, also fixing the quota regression I introduced (when moving the `ENABLE_QUOTA` guard into `_quota_show_for()`).
- `addalias` + `delalias` share common validation check in the new helper, their migrated methods is unaltered.
- The new helper adds some extra documentation.
Adds a common helper method to perform the DB check and list iterations while delegating a method for each to format the output per line/entry as they see fit.

This helper calls an internal method that would be defined earlier by each command. Similar to how `__usage` is called by several methods in `helper.sh` already.
@polarathene polarathene added this to the v12.0.0 milestone Jun 21, 2022
@polarathene polarathene self-assigned this Jun 21, 2022
Copy link
Member Author

@polarathene polarathene left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional commentary if it's helpful.

Just to clarify. Multi-line values in .cf` files is not supported, nor is resolving recursive aliases. This just brings better consistency and reliability to the current support.

Comment on lines +79 to +94
else # Remove target VALUE from entry:
__db_list_already_contains_value || return 0

# The delimiter between key and first value may differ from
# the delimiter between multiple values (value list):
local LEFT_DELIMITER="\(${K_DELIMITER}\|${V_DELIMITER}\)"
# If an entry for KEY contains an exact match for VALUE:
# - If VALUE is the only value => Remove entry (line)
# - If VALUE is the last value => Remove VALUE
# - Otherwise => Collapse value to LEFT_DELIMITER (\1)
sedfile --strict -i \
-e "/^${KEY_LOOKUP}\+${_VALUE_}$/d" \
-e "/^${KEY_LOOKUP}/s/${V_DELIMITER}${_VALUE_}$//g" \
-e "/^${KEY_LOOKUP}/s/${LEFT_DELIMITER}${_VALUE_}${V_DELIMITER}/\1/g" \
"${DATABASE}"
fi
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This handles commands delalias and delmailuser removal of a recipient for an alias(es), and if it was the only recipient, removing the alias too. Both commands call this logic via postfix-virtual.sh delete method which calls db.sh with:

[[ ${MAIL_ALIAS} == '_' ]] && MAIL_ALIAS='\S\+' # delmailuser has a similar pattern but does not scope to a single alias
_db_entry_remove "${DATABASE_VIRTUAL}" "${MAIL_ALIAS}" "${RECIPIENT}"

KEY_LOOKUP is the alias + the delimiter. In some cases the entry may have the delimiter multiple times before the value (eg: white-space with multiple spaces or tabs). As explained below and as the var name implies, it ensures we're looking up the entry key exactly. Avoiding substring mishaps.

The first sed expression uses \+ to deal with any padded delimiter between the key and first value. Usually this isn't necessary.

__db_list_already_contains_value || return 0 is just to avoid sedfile throwing an error if the value doesn't exist. Technically the operation is successful since the result is the same. Could change sedfile --strict to sed instead?


Current related removal code for commands:

sed -i \
-e "/^${EMAIL} *${RECIPIENT}$/d" \
-e "/^${EMAIL}/s/,${RECIPIENT}//g" \
-e "/^${EMAIL}/s/${RECIPIENT},//g" \
"${DATABASE}"

# delete all aliases where the user is the only recipient( " ${EMAIL}" )
# delete user only for all aliases that deliver to multiple recipients ( ",${EMAIL}" "${EMAIL,}" )
if sed -i \
-e "/ ${EMAIL}$/d" -e "s/,${EMAIL}//g" -e "s/${EMAIL},//g" \
"${ALIAS_DATABASE}"

The EMAIL (alias) scope for delalias is vulnerable to substring matching. The wrong alias can be affected as well.

The recipient match in the 2nd sed expression for both commands does not include a right-side delimiter boundary. It is unlikely for the domain part of a recipient to result in a false-positive (due to substring match), but aliases do not need to be fully qualified email addresses either, they may be just the local part which is valid, which may be at more risk of deleting the wrong recipient in addition to the potential target recipient (delalias is explicit, whereas delmailuser is implicit with intent).

The third expression relies on the 2nd one to filter out the concern of a left-side delimiter boundary I think (as the expression applies to the output of the previous one?) EDIT: Nope, each subsequent expression has the full rewritten output to process, not only what was matched/changed.

Example of third sed expression substring match bug causing problems:

EMAIL='sales@example.com'
RECIPIENT='jane@example.com'

sed \
  -e "/^${EMAIL} *${RECIPIENT}$/d" \
  -e "/^${EMAIL}/s/,${RECIPIENT}//g" \
  -e "/^${EMAIL}/s/${RECIPIENT},//g" \
   <<< "sales@example.com jane@example.com,john@example.com,mary-jane@example.com,dave@example.com"

# Result: jane removed, but so was part of mary-jane, now there is "mary-dave"..
sales@example.com john@example.com,mary-dave@example.com

The 2nd sed expression would be vulnerable to the same flaw with domain part, such that a recipient domain of a neighbour could be rewritten to send somewhere else.. TLDs today is a huge list, with plenty of overlap opportunities.

Most likely to result in an accident, rather than an attack to redirect mail to another domain (the attacker probably has better options if they're able to know the recipient list order for an alias, and have control to perform a delete for the desired adjacent recipient).


delmailuser version will delete entire alias mappings if the config is modified by a user. White-space is valid for separating a list of recipients, it's not restricted to , only. Shouldn't be an issue if they're only managing with our commands/setup.sh. The 2nd and third sed expressions have the same flaw, but no longer scoped to a single alias.


New version avoids both substring issues by always ensuring the recipient is matched to the boundaries beside it (delimiter value or end of line).

Comment on lines +64 to +68
( 'append' )
__db_list_already_contains_value && return 1

sedfile --strict -i "/^${KEY_LOOKUP}/s/$/${V_DELIMITER}${VALUE}/" "${DATABASE}"
;;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only used for addalias command.

A separate method checks if the alias already has the recipient added.

  • The error handling is higher level at postfix-virtual.sh based on status code returned.
  • That method is re-used for remove and could also help listmailuser alias list retrieval I think (which I did not find time to tackle).

Replaces that commands equivalent logic:

grep \
-qi "^$(_escape "${EMAIL}")[a-zA-Z@.\ ]*$(_escape "${RECIPIENT}")" \
"${DATABASE}" 2>/dev/null && _exit_with_error "Alias \"${EMAIL} ${RECIPIENT}\" already exists"
if grep -qi "^$(_escape "${EMAIL}")" "${DATABASE}" 2>/dev/null
then
sed -i "/${EMAIL}/s/$/,${RECIPIENT}/" "${DATABASE}"
else
echo "${EMAIL} ${RECIPIENT}" >> "${DATABASE}"
fi

EMAIL for sed was not escaped, probably because of mixed usage with echo. While db.sh here handles that appropriately further down below. KEY_LOOKUP contains the KEY (alias) escaped and with the delimiter to avoid the substring match, recipient won't be at risk of being added to the wrong alias.

The earlier check for the alias in the DB was already handled in db.sh a few lines above, most commands that need to perform that check now use that common method instead of copy/paste grep -qi (and inconsistent usage of escaping which is it's own bug).

The earlier check for the recipient has a flaw due to restricting characters between the alias and the target recipient lookup to [a-zA-Z@.\ ]*. Ignoring support for unicode, this didn't allow - or _ either, a recipient in that search-space with - or similar will ignore any valid match and allow appending the same recipient multiple times.

Comment on lines +70 to +73
( 'replace' )
ENTRY=$(__escape_sed_replacement "${ENTRY}")
sedfile --strict -i "s/^${KEY_LOOKUP}.*/${ENTRY}/" "${DATABASE}"
;;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the most common operation beyond removal. Replaces the value in an entry for a key by rewriting the entire entry.

Most commands were using this, or pairing it with echo if no other value existed (which db.sh handles further down with the same ENTRY var when the key does not exist for an entry).

The updatemailuser + updatedovecotmasteruser commands had a rather ugly looking sed command due to the password hash being base64 encoded (there's other encoding options, but for whatever reason that wasn't considered at the time), that includes the / character in the base64 "alphabet", thus problematic in a sed command:

sed -i "s ^""${USER}""|.* ""${USER}""|""${HASH}"" " "${DATABASE}"

__escape_sed_replacement fixes that by escaping / and &, it does not escape \ - which I'd rather we had a check to reject on among other special characters instead of escaping them.


Some commands have since been updated to be smarter with the replace approach, but previously, and with the current setquota, there is a call to a delete command to then add the value as new, instead of update the existing one:

delquota "${USER}"
echo "${USER}:${QUOTA}" >>"${DATABASE}"

It makes sense to maintain a single command for the same generic functionality IMO.

The quota commands aren't escaping the target account either, not that a single wildcard . has much likelihood of matching the wrong user (at least domain part, local part may be at more risk with enough users).


Each relay command leverages this. They roughly had the same pattern (exclude being slightly different):

if grep -qi "^@${DOMAIN}" "${DATABASE}" 2>/dev/null
then
sed -i \
"s|^@${DOMAIN}.*|@${DOMAIN}\t\t[${HOST}]:${PORT}|" \
"${DATABASE}"
else
echo -e "@${DOMAIN}\t\t[${HOST}]:${PORT}" >>"${DATABASE}"
fi

if grep -qi "^@${DOMAIN}" "${DATABASE}" 2>/dev/null
then
sed -i \
"s|^@${DOMAIN}.*|@${DOMAIN}\t\t${USER}:${PASSWD}|" \
"${DATABASE}"
else
echo -e "@${DOMAIN}\t\t${USER}:${PASSWD}" >>"${DATABASE}"
fi

if grep -qi "^@${DOMAIN}" "${DATABASE}" 2>/dev/null
then
sed -i "s/^@${DOMAIN}.*/@${DOMAIN}/" "${DATABASE}"
else
echo -e "@${DOMAIN}" >> "${DATABASE}"
fi

Comment on lines +28 to +57
function _db_entry_add_or_append { _db_operation 'append' "${@}" ; } # Only used by addalias
function _db_entry_add_or_replace { _db_operation 'replace' "${@}" ; }
function _db_entry_remove { _db_operation 'remove' "${@}" ; }

function _db_operation
{
local DB_ACTION=${1}
local DATABASE=${2}
local KEY=${3}
# Optional arg:
local VALUE=${4}

# K_DELIMITER provides a match boundary to avoid accidentally matching substrings:
local K_DELIMITER KEY_LOOKUP
K_DELIMITER=$(__db_get_delimiter_for "${DATABASE}")
# Due to usage in regex pattern, KEY needs to be escaped:
KEY_LOOKUP="$(_escape "${KEY}")${K_DELIMITER}"

# Support for adding or replacing an entire entry (line):
# White-space delimiter should be written into DATABASE as 'space' character:
local V_DELIMITER="${K_DELIMITER}"
[[ ${V_DELIMITER} == '\s' ]] && V_DELIMITER=' '
local ENTRY="${KEY}${V_DELIMITER}${VALUE}"

# Support for 'append' + 'remove' operations on value lists:
# NOTE: Presently only required for `postfix-virtual.cf`.
local _VALUE_
_VALUE_=$(_escape "${VALUE}")
# `postfix-virtual.cf` is using `,` for delimiting a list of recipients:
[[ ${DATABASE} == "${DATABASE_VIRTUAL}" ]] && V_DELIMITER=','
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This section is a bit noisy/verbose. The inline comments should document it fairly well, but to re-iterate:

  • Convenience methods handle DB_ACTION and DATABASE args, those methods themselves are called with the KEY and VALUE args. Presently the only time VALUE is optional is with the excluderelaydomain command (replace) and when performing delete for postfix-accounts.cf/dovecot-masters.cf + dovecot-quotas.cf databases/configs (postfix-virtual.cf always provides a VALUE).
  • _db_has_entry_with_key defined near the bottom of db.sh shares the K_DELIMITER block, K_ being a prefix for the KEY delimiter which __db_get_delimiter_for provides.
  • Usually the delimiter is the same (eg: | for postfix-accounts.cf), but for white-space we want to match any with \s and default to (space) for writing, V_DELIMITER (V_ being prefix for VALUE) helps distinguish it's usage instead of overloading a single DELIMITER.
  • ENTRY (for adding/replacing an entry) needs V_DELIMITER, but after that the usage is overloaded (if needed, presently only for postfix-virtual.cf to a delimiter for when VALUE is an array/list - handled in append and remove.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually like it. If in doubt, I'd go with more verbose than less verbose, for "future generations" :D

Comment on lines +124 to +134
# Internal method for: _db_operation
function __db_list_already_contains_value
{
# Avoids accidentally matching a substring (case-insensitive acceptable):
# 1. Extract the current value of the entry (`\1`),
# 2. If a value list, split into separate lines (`\n`+`g`) at V_DELIMITER,
# 3. Check each line for an exact match of the target VALUE
sed -e "s/^${KEY_LOOKUP}\(.*\)/\1/" \
-e "s/${V_DELIMITER}/\n/g" \
"${DATABASE}" | grep -qi "^${_VALUE_}$"
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A more reliable implementation that these two methods cover (listmailuser presently does not use it, so isn't as robust currently):

grep \
-qi "^$(_escape "${EMAIL}")[a-zA-Z@.\ ]*$(_escape "${RECIPIENT}")" \
"${DATABASE}" 2>/dev/null && _exit_with_error "Alias \"${EMAIL} ${RECIPIENT}\" already exists"

if [[ -f ${ALIASES} ]] && grep -q "${USER}" "${ALIASES}"

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So should we change listmailuser to use this as well?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So should we change listmailuser to use this as well?

The concern is raised in the listmailuser review comment. A check that is more robust won't be as helpful if the actual logic that is performed is problematic itself 😅

Comment on lines +25 to +29
local PASSWD_HASH
PASSWD_HASH=$(doveadm pw -s SHA512-CRYPT -u "${MAIL_ACCOUNT}" -p "${PASSWD}")
# Early failure above ensures correct operation => Add (create) or Replace (update):
_db_entry_add_or_replace "${DATABASE}" "${MAIL_ACCOUNT}" "${PASSWD_HASH}"
;;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

create and update could be split with a bit of code duplication, it wouldn't be that harmful with co-location. That wouldn't really change the DB operation being called though (presently both add and update are under the replace action/operation, just different branches based on if the KEY/account exists in the DB).


This doveadm command isn't actually using -u. Dovecot docs for the pw command note that it's only relevant for a different digest scheme (-s). It could be removed.

echo -e " [ aliases -> $(grep "${USER}" "${ALIASES}" | awk '{print $1;}' | sed ':a;N;$!ba;s/\n/, /g')]\n"
else
echo
grep "${MAIL_ACCOUNT}" "${DATABASE_VIRTUAL}" | awk '{print $1;}' | sed ':a;N;$!ba;s/\n/, /g'
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't get around to trying to interpret what sed ':a;N;$!ba;s/\n/, /g' translates into. As such I didn't bother to have _account_has_an_alias leverage the more robust __db_list_already_contains_value method (which only returns a status).

I understand the grep here will return any lines from the postfix-virtual.cf database file regardless of substring matches (including alias keys), and then do some incantation with those lines to extract the aliases (keys) that matches were found?

`source-path` added to `index.sh` is sufficient if used directly above the `source` line, otherwise it can be placed early on in the sourced file, but must be before the dependent `source` lines appear (function scope is ignored).

Likewise, I forgot that the `PATH_TO_SCRIPTS` var is only for a common ancestor of the real path and shellchecks source path, it's stripped/ignored by shellcheck in favor of trying any known source-path. So `/manage` must be added to each of these `source` lines instead.

The prior `source-path` applied within the function, as noted only applied to the direct line beneath it, not the entire scope.

Added comments for future maintainers to be aware of. Decided to keep it all within this `db.,sh` file, rather than `helpers/index.sh` , although perhaps that's a better place to document/demonstrate such. Otherwise `test/linting/lint.sh` covers this advice for the most part.
Copy link
Member

@georglauterbach georglauterbach left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, I took some time to look at everything, but this is a rather large PR, so there is a fair chance of missing something. But as far as I can tell, LGTM 👍🏼

@polarathene polarathene requested a review from a team July 2, 2022 02:40
@polarathene

This comment was marked as outdated.

@casperklein

This comment was marked as outdated.

@casperklein

This comment was marked as outdated.

@polarathene

This comment was marked as outdated.

@georglauterbach

This comment was marked as resolved.

@polarathene

This comment was marked as resolved.

@georglauterbach

This comment was marked as resolved.

@polarathene polarathene modified the milestones: v12.0.0, v11.2.0 Jul 13, 2022
@georglauterbach

This comment was marked as outdated.

Copy link
Member

@casperklein casperklein left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, I took some time to look at everything, but this is a rather large PR, so there is a fair chance of missing something. But as far as I can tell, LGTM 👍🏼

I second this. Also the tests all pass, so I finally approve this 🎉

@polarathene polarathene merged commit 57aeb6d into docker-mailserver:master Jul 29, 2022
@polarathene
Copy link
Member Author

polarathene commented Jul 29, 2022

Thanks to both of you for taking the time to review. Again my apologies for the size, I realize how daunting that'd be to review 😅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants