Public mailboxes with Apple mail client

NethServer Version: 7.9.2009
Module: mail

I’m trying to configure public mailboxes in nethserver for use on Apple Mail clients. For whatever reason it seems that Apple requires a dot (".") as imap separator in order to show the public mailboxes.

The problem now is this: I can change the separator in dovecot by creating a custom template file for /etc/dovecot/dovecot.conf/40namespaces and specfify
separator = .
prefix = Shared.%%n@{{ $DomainName }}.

This will make the shared folder show up in Apple Mail. Once I do so, they are no longer visible in Cockpit and I cannot set permissions etc.

Is there an easy way to make Cockpit aware of the changed separator?

Kind regards,

Oliver

see this post Special Mail Folders at Nextcloud - #4 by Shane_Treweek

your fix “should” be as simple as adding the directive for the equivalent of prefix = Shared.%%n@{{ $DomainName }}

in the way described in the link it allows the folder to be broadcasted as multiple labels to make it compatible as for example outlook has there own naming convention for spam (junk i think)

Thanks for your answer, but I’m afraid it doesn’t solve my problem.

Specifically, the dovecot configuration is fine as far as the mail client is concerned. The only problem is that with the configuration changes that are required for the client, Cockpit no longer shows the public mailboxes I’ve created.

At the moment it’s either

  • use the default configuration, edit my public mailboxes in Cockpit => no access to public mailboxes from Apple Mail
  • use my custom configuration => public mailboxes disappear in Cockpit but are usable in Apple Mail

If I could Cockpit make aware of the changed separator I think I’d have both working.

Copy following file to home dir in case something goes wrong:

cp /usr/libexec/nethserver/api/nethserver-mail/mailbox/read ~

Add your domain to the list command in /usr/libexec/nethserver/api/nethserver-mail/mailbox/read line 35:

my @list = `/usr/bin/doveadm mailbox list -u vmail Shared.%n\@yourdomain.example.org.*`;

Raise the number to trim to 10 at line 39:

my $name = substr($_, 10); # trim Public/ prefix

This way the public folders are visible in Cockpit but I don’t know if this change has negative side effects…

ive checked on my mac and get the public folders and there accessible on nethserver

just incase ive modified my configs in the past and forgot here is my read file and my dovecot.conf

read file
#!/usr/bin/perl

#
# Copyright (C) 2019 Nethesis S.r.l.
# http://www.nethesis.it - nethserver@nethesis.it
#
# This script is part of NethServer.
#
# NethServer is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License,
# or any later version.
#
# NethServer is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with NethServer.  If not, see COPYING.
#

use strict;
use warnings;
use esmith::ConfigDB;
use JSON;

require '/usr/libexec/nethserver/api/lib/helper_functions.pl';
require '/usr/libexec/nethserver/api/nethserver-mail/lib/mail_functions.pl';

sub list_public_folders
{
    my $expand = shift;
    my %folders;
    my @list = `/usr/bin/doveadm mailbox list -u vmail Public/*`;
    my $junkFolder = esmith::ConfigDB->open_ro()->get_prop('dovecot', 'SpamFolder') || '';
    foreach (@list) {
        chomp;
        my $name = substr($_, 7); # trim Public/ prefix
        next if (index($name, '/') >= 0);
        $folders{$name} = {
            'name' => $name,
            'type' => 'public',
            'readonly' => ($name eq $junkFolder),
            'displayname' => $name
        };
        if ($expand) {
            $folders{$name}{'acls'} = get_folder_acls($name);
        }
    }

    return \%folders;
}

my $input = readInput();
my $cmd = $input->{'action'};

my $ret = {};

if ($cmd eq 'list') {
    $ret->{'builtin'} = [{"name" => "root", "type" => "builtin", "displayname" => "root"}];
    $ret->{'users'} = [];
    $ret->{'groups'} = [];
    $ret->{'public'} = [];
    my $expand = $input->{'expand'} || 0;
    my $cdb = esmith::ConfigDB->open_ro();
    my $adb = esmith::ConfigDB->open_ro('accounts');
    my ($gdb, $quotas, $connectors, $quota_status);
    if ($expand) {
        $gdb = esmith::ConfigDB->open_ro('getmail');
        $quotas = decode_json(`/usr/libexec/nethserver/mail-quota`);
        $quota_status =  $cdb->get_prop('dovecot', 'QuotaStatus');
    }
    my $users = safe_decode_json(`/usr/libexec/nethserver/list-users`, []);
    my $dynamic = $cdb->get_prop('postfix', 'DynamicGroupAlias') || 'disabled';

    if ($expand) {
        if ($gdb) {
            foreach my $gr ($gdb->get_all()) {
                my $obj = {'type' => 'getmail', 'name' => $gr->key, 'props' => {}};
                my %props = $gr->props;
                my $account = $props{'Account'};
                delete($props{'Account'});
               $obj->{'props'} = \%props;
                if (!$connectors->{$account}) {
                    $connectors->{$account} = [];
                }
                push($connectors->{$account}, $obj);
            }
        }
    }

    foreach (keys %$users) {
        my $status = $adb->get_prop($_, 'MailStatus') || 'enabled';
        $_ =~ m/(.*)\@(:*)/;
        my $user = {
            'name' => $_,
            'displayname' => $1,
            'type' => 'user'
        };
        if ($expand) {
            $user->{'props'} = get_defaults('user', $cdb);
            $user->{'quota'} = { 'percentage' => 0, 'messages' => 0, 'maximum' => 0, 'size' => 0};
            $user->{'connectors'}  = $connectors->{$_} ? $connectors->{$_} : {};

            my $record = $adb->get($_) || undef;
            if (defined($record)) {
                # read config from db
                my %props = $record->props;
                $props{'MailSpamRetentionTime'} =  substr($props{'MailSpamRetentionTime'} || '', 0, -1); # remove final 'd'
                $props{'MailForwardAddress'} = [split(',', $props{'MailForwardAddress'} || '')];
                # force custom quota to default  if global quota is disabled
                if ($props{'MailQuotaType'} && $quota_status eq 'disabled') {
                    $props{'MailQuotaType'} = 'default';
                }
                # express quota in GB
                if ($props{'MailQuotaCustom'} && $props{'MailQuotaCustom'} ne 'unlimited') {
                    $props{'MailQuotaCustom'} = int($props{'MailQuotaCustom'}/10);
                }
                foreach my $k (keys %props) {
                    $user->{'props'}{$k} = $props{$k};
                }
            }
            my $quota = $quotas->{$_} || undef;
            if ($quota) {
                $user->{'quota'}{'percentage'} = int($quota->{'perc'});
                $user->{'quota'}{'messages'} = $quota->{'msg'};
                $user->{'quota'}{'size'} = int($quota->{'size'})*1024;
                $user->{'quota'}{'maximum'} = int($quota->{'max'})*1024;
            }
            push($ret->{'users'}, $user);
        } else {
            # add to flat list only if enabled
            push($ret->{'users'}, $user) if ($status eq 'enabled');
        }
    }

    my $folders = list_public_folders($expand);
    # output group list
    #   if expanded == true -> the list is used under the groups mailboxes
    #   if expanded == false -> the list is user for ACL on public mailboxes
    if ($dynamic eq 'enabled' || !$expand) {
        my $groups = safe_decode_json(`/usr/libexec/nethserver/list-groups`, []);
        foreach my $group (keys %$groups) {
            $group =~ m/(.*)\@(:*)/;
            my $obj = {'name' => $group, 'displayname' => $1, 'type' => 'group'};
            my $status = $adb->get_prop($group, 'MailStatus') || 'enabled';
            if ($expand) {
                # a group is enabled if DynamicGroupAlias is enabled and the record on accounts db is not disabled
                $obj->{'props'} = { 'MailStatus' => $status };
                push($ret->{'groups'}, $obj);
            } else {
                # add to flat list only if enabled
                push($ret->{'groups'}, $obj) if ($status eq 'enabled');
            }
        }
    }

    foreach my $folder (keys %$folders) {
        push($ret->{'public'}, $folders->{$folder});
    }
} elsif ($cmd eq 'configuration') {

    my $config = {};
    my $cdb = esmith::ConfigDB->open_ro();
    my $dovecot = $cdb->get('dovecot');

    foreach (qw(AdminIsMaster DeletedToTrash ImapStatus LogActions MaxUserConnectionsPerIp PopStatus QuotaStatus)) {
        $config->{$_} = $dovecot->prop($_);
    }
    $config->{'QuotaDefaultSize'} = int($dovecot->prop('QuotaDefaultSize')/10);
    my $retention = $dovecot->prop('SpamRetentionTime');
    if ($retention eq 'infinite') {
        $config->{'SpamRetentionTime'} = "-1";
    } else {
        $config->{'SpamRetentionTime'} =  substr($retention, 0, -1); # remove 'd' suffix
    }
    $config->{'SpamFolder'} =  ($dovecot->prop('SpamFolder') ne '') ? 'enabled' : 'disabled';
    $config->{'TlsSecurity'} =  ($dovecot->prop('TlsSecurity') eq 'required') ? 'enabled' : 'disabled';
    $config->{'DynamicGroupAlias'} = $cdb->get_prop('postfix', 'DynamicGroupAlias') || 'disabled';

    $ret->{'configuration'} = $config;

} elsif ($cmd eq 'aliases') {
    my $name = $input->{'name'};
    my $type = $input->{'type'};
    my @aliases;
    my $adb = esmith::ConfigDB->open_ro('accounts');
    my $ddb = esmith::ConfigDB->open_ro('domains');
    my $cdb = esmith::ConfigDB->open_ro();
    my $dynamic = $cdb->get_prop('postfix', 'DynamicGroupAlias') || 'disabled';

    my $domain = $cdb->get_value('DomainName');
    my $public_domain = defined($ddb->get($domain));

    # add built-in alias if local domain is also a mail domain
    if ($type eq 'user' && $public_domain) {
        # user must be added only if MailStatus == enabled
        my $mail_status = $adb->get_prop($name, 'MailStatus') || 'enabled';
        push(@aliases, $name) if ($mail_status eq 'enabled');
    }
    if ($type eq 'group') {
        if ($dynamic eq 'enabled' && $public_domain) {
            push(@aliases, $name);
        } else {
            $name =~ m/(.*)\@(:*)/;
            $name = "$name|vmail\\+".$1;
        }
    }
    if ($type eq 'public') {
        $name = "vmail\\+$name\$";
    }

    my @domains = $ddb->get_all_by_prop('type' => 'domain');

    foreach my $pseudonym ($adb->get_all_by_prop('type' => 'pseudonym')) {
        my @accounts = split(",", $pseudonym->prop('Account') || '');
        foreach my $a (@accounts) {
            if ($a =~ m/$name/) {
                # expand wildcard
                if ($pseudonym->key =~ m/\@$/) {
                    foreach my $d (@domains) {
                        push(@aliases, $pseudonym->key.$d->key);
                    }
                } else {
                    push(@aliases, $pseudonym->key);
                }
            }
        }
    }

    $ret->{'aliases'} = \@aliases;

} else {
    error();
}

print encode_json($ret);
dovecot.conf
# ================= DO NOT MODIFY THIS FILE =================
#
# Manual changes will be lost when this file is regenerated.
#
# Please read the developer's guide, which is available
# at NethServer official site: https://www.nethserver.org
#
#
#
# disable debug
#
debug_log_path = /dev/null


mbox_write_locks = fcntl
ssl_cert = </etc/pki/dovecot/certs/dovecot.pem
ssl_key = </etc/pki/dovecot/private/dovecot.pem
mail_shared_explicit_inbox = yes





#
# TLS
# cipher selection 2020-05-10 Only TLS 1.2 (RSA and ECC certificate)
#
ssl_dh_parameters_length = 2048

ssl_protocols = !SSLv3 !SSLv2 !TLSv1 !TLSv1.1

ssl_cipher_list = kEECDH:+kEECDH+SHA:kEDH:+kEDH+SHA:+kEDH+CAMELLIA:kECDH:+kECDH+SHA:kRSA:+kRSA+SHA:+kRSA+CAMELLIA:!aNULL:!eNULL:!SSLv2:!SSLv3:!RC4:!MD5:!DES:!EXP:!SEED:!IDEA:!3DES

ssl_prefer_server_ciphers = yes


#
# 10environment
#
import_environment = TZ KRB5CCNAME

#
# Limits
#

default_process_limit = 400

mail_max_userip_connections = 12

service anvil {
    client_limit = 1603
}

service auth {
    client_limit = 2400
}



#
# 10limits_local
#
remote 127.0.0.1 {
  mail_max_userip_connections = 60
}
#
# 20protocols -- configure access to mailboxes
#
protocols = imap lmtp sieve pop3
disable_plaintext_auth = no

# inet_listener control for IMAP protocol. Refs #1396
service imap-login {
    inet_listener imap {
        port = 143
    }
    inet_listener imaps {
        port = 993
    }
}

#
# 20users -- configure user authentication and storage
#
first_valid_uid = 981
last_valid_uid = 981

mail_uid = vmail
mail_gid = vmail

# See http://wiki2.dovecot.org/Authentication/MasterUsers
auth_master_user_separator = *
passdb {
  driver = passwd-file
  args = /etc/dovecot/master-users
  master = yes
}



# The list of disabled user accounts
passdb {
  driver = passwd-file
  args = /etc/dovecot/deny.passwd
  deny = yes
}

# Authenticate users against PAM
passdb {
  driver = pam
  args = max_requests=100 failure_show_msg=yes blocking=yes
}

# LDA/IMAP access for users in /etc/passwd without @domain in username
userdb {
  driver = passwd-file
  args = username_format=%Ln /etc/dovecot/imap.passwd
  override_fields = uid=981 gid=978 home=/var/lib/nethserver/vmail/%Ln
}

# User custom quota userdb override:
# Global quota is disabled

# LDA/IMAP access for users from SSSD/NSS
userdb {
  skip = found
  driver = passwd
  override_fields = uid=981 gid=978 home=/var/lib/nethserver/vmail/%u
}

# catch-all mailbox
# disabled

# Location of mailboxes:
mail_location = maildir:~/Maildir

#
# 25user-action_logs
#
mail_plugins = $mail_plugins mail_log notify
plugin {
  mail_log_events = delete undelete expunge copy mailbox_delete mailbox_rename mailbox_create flag_change append
  mail_log_fields = uid box msgid from subject flags
}


#
# 30quota -- uncomment to load the quota plugin globally
#
# mail_plugins = $mail_plugins quota


#
# Namespace setup
#

# Private mailboxes
namespace ROOT {
  type = private
  separator = /
  prefix =
  # location defaults to mail_location.
  inbox = yes
  subscriptions = yes

  # Commonly used folders:
  mailbox Trash {
    special_use = \Trash
    auto = no
  }
  mailbox Drafts {
    special_use = \Drafts
    auto = no
  }
  mailbox Sent {
    special_use = \Sent
    auto = no
  }
  mailbox "Sent Messages" {
    special_use = \Sent
    auto = no
  }

}

# Shared mailboxes are enabled
namespace SHARED_USERS {
  type = shared
  disabled = no
  separator = /
  prefix = Shared/%%n@domain.com/
  location = maildir:/var/lib/nethserver/vmail/%%u/Maildir
  subscriptions = no
  list = children
}

# Public mailboxes
namespace PUBLIC {
  type = public
  separator = /
  prefix = Public/
  subscriptions = no
  list = children
  location = maildir:/var/lib/nethserver/vmail/vmail/Maildir:INDEXPVT=~/Maildir/public
}

# Enable acl plugin for shared mailboxes,
# and listescape to extend allowable characters in mailbox names
mail_plugins = $mail_plugins acl listescape fts fts_lucene
protocol imap {
  mail_plugins = $mail_plugins imap_acl
}

plugin {
  acl = vfile
  acl_shared_dict = file:/var/lib/nethserver/vmail/shared-mailboxes.db
  fts = lucene
  fts_lucene = whitespace_chars=@.
}

service dict {
  unix_listener dict {
    mode = 0600
    user = vmail
  }
}

service imap-login {
   unix_listener imap-ipc {
     group = root
     user = $default_internal_user
     mode = 0600
   }
}

# auth_debug = yes
# auth_verbose = yes
#
# 40postlogin (imap, managesieve)
#

service imap {
  executable = imap imap-postlogin
}

service imap-postlogin {
  executable = script-login /usr/libexec/nethserver/dovecot-postlogin
  user = $default_internal_user
  unix_listener imap-postlogin {
  }
}

service managesieve {
  executable = managesieve sieve-postlogin
}

service sieve-postlogin {
  executable = script-login /usr/libexec/nethserver/dovecot-postlogin
  user = $default_internal_user
  unix_listener sieve-postlogin {
  }
}

service pop3 {
  executable = pop3 pop3-postlogin
}

service pop3-postlogin {
  executable = script-login /usr/libexec/nethserver/dovecot-postlogin
  user = $default_internal_user
  unix_listener pop3-postlogin {
  }
}

#
# 50deletedtotrash dovecot plugin
#
protocol imap {
	mail_plugins = $mail_plugins deleted_to_trash
}
plugin {
	deleted_to_trash_folder = Trash
}



#
# Quota configuration is disabled
#


#
# 50spam_training
#
protocol imap {
  mail_plugins = $mail_plugins imap_sieve
}

plugin {
    sieve_plugins = sieve_extprograms sieve_imapsieve
    sieve_pipe_bin_dir = /usr/libexec/nethserver/imapsieve
    sieve_global_extensions = +vnd.dovecot.pipe +vnd.dovecot.environment

    # From elsewhere to Spam folder
    imapsieve_mailbox1_name = Junk
    imapsieve_mailbox1_causes = COPY
    imapsieve_mailbox1_before = file:/var/lib/nethserver/sieve-scripts/report-spam.sieve

    # From Spam folder to elsewhere
    imapsieve_mailbox2_name = *
    imapsieve_mailbox2_from = Junk
    imapsieve_mailbox2_causes = COPY
    imapsieve_mailbox2_before = file:/var/lib/nethserver/sieve-scripts/report-ham.sieve

}

#
# 40spamfolder
#
namespace ROOT {
  # Our junkmail folder:
  mailbox Junk {
    special_use = \Junk
    auto = subscribe
  }
}


#
# 50vsz_limit
#
# Using default value for default_vsz_limit

#
# LDA message delivery for getmail
#

protocol lda {
    mail_plugins = $mail_plugins sieve
}

#
# LMTP server for message delivery
#

protocol lmtp {
    mail_plugins = $mail_plugins sieve
}

recipient_delimiter = +
lmtp_save_to_detail_mailbox = yes

# Global sieve script, executed BEFORE user's private scripts:
plugin {
    sieve_before = /var/lib/nethserver/sieve-scripts/before.sieve
    sieve_after = /var/lib/nethserver/sieve-scripts/after.sieve
    sieve_extensions = +imapflags +editheader
}

service lmtp {

    user = vmail
    client_limit = 1

    unix_listener lmtp {
        user = vmail
        group = vmail
        mode = 00660
    }

}

service auth {
    unix_listener auth-userdb {
       mode = 00660
       # user = <default>
       group = vmail
    }
}

#
# Postfix to Dovecot communcations for SMTP AUTH
# See http://www.postfix.org/SASL_README.html
#

service auth {
    unix_listener smtpauth {
       path = /var/spool/postfix/private/smtpauth
       mode = 00660
       user = postfix
       group = postfix
    }
}

auth_mechanisms = plain login gssapi
auth_krb5_keytab = /var/lib/dovecot/krb5.keytab

also not sure if it makes a difference but my mac is older and runs high sierra (thought id mention incase it works on mine but not on yours if yours is newer and apple changed the code)

Markus, Shane, thank you both for your answers.

I got it to work with the solution from Markus first. However, since Shane said that it worked for him without modifications, I dug a bit deeper.

The problem was that I could not change my subscription settings to the public folder in Apple Mail with the separator set to “/”. This turned out to be some kind of caching problem in Apple Mail. Once I deleted the whole IMAP account in Apple Mail and created it again it worked.

Long story short: If I hadn’t meddled with the separator settings in the beginning (did this a couple of years ago, can’t remember why) it would have worked as it should.

2 Likes