feat(carddav): fix issue where only personal contacts can be synced on macOS (#1981). When , append the addressbook name as a username suffix to force macOS to treat each book as a distinct account. Usernames must not contain . Install via the provisioning profile. Closes #6033.

This commit is contained in:
smizrahi committed 2026-05-11 14:21:35 +02:00
1 parent d16a1a9708
commit 761934af42
13 files changed
+136 -9

No files matched your search

+15 -3
View File
@@ -49,7 +49,7 @@
- (BOOL) checkLogin: (NSString *) _login
password: (NSString *) _pwd
{
NSString *domain;
NSString *domain, *checkedLogin;
SOGoSystemDefaults *sd;
SOGoCASSession *session;
SOGoPasswordPolicyError perr;
@@ -58,9 +58,21 @@
domain = nil;
perr = PolicyNoError;
sd = [SOGoSystemDefaults sharedSystemDefaults];
checkedLogin = [_login stringByReplacingString: @"%40" withString: @"@"];
/* Per-addressbook CardDAV profile: a login of the form <user>!<book> is an
alias used to discriminate the URL path; the password check applies to
the underlying real user. */
if ([sd carddavSingleAddressBookProfile])
{
NSRange aliasRange = [checkedLogin rangeOfString: @"!"];
if (aliasRange.location != NSNotFound)
checkedLogin = [checkedLogin substringToIndex: aliasRange.location];
}
rc = ([[SOGoUserManager sharedUserManager]
checkLogin: [_login stringByReplacingString: @"%40"
withString: @"@"]
checkLogin: checkedLogin
password: _pwd
domain: &domain
perr: &perr
+30 -2
View File
@@ -22,6 +22,7 @@
#import <Foundation/Foundation.h>
#import "SOGoMobileProvision.h"
#import "SOGoSystemDefaults.h"
#import "SOGoUser.h"
#import "NSString+Crypto.h"
@@ -34,11 +35,12 @@
NSURL *serverURL;
NSDictionary *provisioning;
NSError *error;
NSString *payloadType, *prefix, *type;
NSString *payloadType, *prefix, *type, *username;
NSNumber *port;
activeUser = [context activeUser];
serverURL = [context serverURL];
username = [activeUser login];
if (!context || !path) {
[self errorWithFormat: @"Invalid provisioning profile - no parameters"];
@@ -55,6 +57,32 @@
payloadType = @"com.apple.carddav.account";
prefix = @"CardDAV";
type = @"Contact";
/* Per-addressbook CardDAV profile: rewrite the principal URL and the
CardDAV username from <user> to <user>!<book>. macOS Contacts
strips path-only discriminators when it falls back on its own
discovery; embedding the alias in the username keeps the !book
tag in every subsequent request, both in the URL and in the Basic
Auth header. */
if ([[SOGoSystemDefaults sharedSystemDefaults] carddavSingleAddressBookProfile])
{
NSArray *parts;
parts = [path componentsSeparatedByString: @"/"];
if ([parts count] >= 7
&& [[parts objectAtIndex: 4] isEqualToString: @"Contacts"])
{
NSString *aliasLogin;
NSMutableArray *rewritten;
aliasLogin = [NSString stringWithFormat: @"%@!%@",
[parts objectAtIndex: 3],
[parts objectAtIndex: 5]];
rewritten = [parts mutableCopy];
[rewritten replaceObjectAtIndex: 3 withObject: aliasLogin];
path = [rewritten componentsJoinedByString: @"/"];
[rewritten release];
username = aliasLogin;
}
}
break;
}
@@ -74,7 +102,7 @@
port, [NSString stringWithFormat:@"%@%@", prefix, @"Port"],
path, [NSString stringWithFormat:@"%@%@", prefix, @"PrincipalURL"],
[NSNumber numberWithBool:[[serverURL scheme] isEqualToString:@"https"]], [NSString stringWithFormat:@"%@%@", prefix, @"UseSSL"],
[activeUser login], [NSString stringWithFormat:@"%@%@", prefix, @"Username"],
username, [NSString stringWithFormat:@"%@%@", prefix, @"Username"],
[NSString stringWithFormat: @"SOGo %@ provisioning", prefix], @"PayloadDescription",
[NSString stringWithFormat:@"%@ %@", type, name], @"PayloadDisplayName",
[NSString stringWithFormat:@"%@.%@.apple.%@", [serverURL host], [name asMD5String], [prefix lowercaseString]], @"PayloadIdentifier",
+4 -1
View File
@@ -1257,7 +1257,10 @@
davCurrentUserPrincipal = nil;
else
{
s = [NSString stringWithFormat: @"/SOGo/dav/%@", login];
/* loginAlias preserves the <user>!<book> alias used by the
per-addressbook CardDAV profile, so that macOS Contacts keeps
using the alias URL instead of falling back on the canonical one. */
s = [NSString stringWithFormat: @"/SOGo/dav/%@", [activeUser loginAlias]];
userHREF = davElementWithContent (@"href", XMLNS_WEBDAV, s);
davCurrentUserPrincipal
= [davElementWithContent (@"current-user-principal",
+1
View File
@@ -62,6 +62,7 @@ static const NSString *kDisableSharingCalendar = @"Calendar";
- (BOOL) isCalendarDAVAccessEnabled;
- (BOOL) isCalendarJitsiLinkEnabled;
- (BOOL) isAddressBookDAVAccessEnabled;
- (BOOL) carddavSingleAddressBookProfile;
- (BOOL) enableEMailAlarms;
+5
View File
@@ -588,6 +588,11 @@ _injectConfigurationFromFile (NSMutableDictionary *defaultsDict,
return [self boolForKey: @"SOGoAddressBookDAVAccessEnabled"];
}
- (BOOL) carddavSingleAddressBookProfile
{
return [self boolForKey: @"SOGoCarddavSingleAddressBookProfile"];
}
- (BOOL) enableEMailAlarms
{
return [self boolForKey: @"SOGoEnableEMailAlarms"];
+5
View File
@@ -63,6 +63,10 @@
SOGoUserFolder *homeFolder;
NSString *currentPassword;
NSString *loginInDomain;
/* The login as it appeared at lookup time, before any alias stripping
(per-addressbook CardDAV profile uses <user>!<book> aliases). Equal
to [self login] for regular users. */
NSString *loginAlias;
//NSString *language;
NSArray *allEmails;
NSMutableArray *mailAccounts;
@@ -88,6 +92,7 @@
- (NSString *) currentPassword;
- (NSString *) loginInDomain;
- (NSString *) loginAlias;
/* properties */
- (NSString *) domain;
+21
View File
@@ -144,11 +144,26 @@
realUID = nil;
domain = nil;
ASSIGN (loginAlias, newLogin);
if ([newLogin isEqualToString: @"anonymous"] || [newLogin isEqualToString: @"freebusy"])
realUID = newLogin;
else
{
sd = [SOGoSystemDefaults sharedSystemDefaults];
/* Per-addressbook CardDAV profile: strip the alias suffix so that a login
of the form <user>!<book> resolves to the real user <user>. The book
part is consumed elsewhere (Main/SOGo.m, SOGoContactFolders) to filter
the addressbook listing. The original alias is kept in loginAlias so
that DAV property hrefs (current-user-principal, etc.) preserve it. */
if ([sd carddavSingleAddressBookProfile])
{
NSRange aliasRange = [newLogin rangeOfString: @"!"];
if (aliasRange.location != NSNotFound)
newLogin = [newLogin substringToIndex: aliasRange.location];
}
if ([sd enableDomainBasedUID] || [[sd loginDomains] count] > 0)
{
r = [newLogin rangeOfString: @"@" options: NSBackwardsSearch];
@@ -243,9 +258,15 @@
[currentPassword release];
[cn release];
[loginInDomain release];
[loginAlias release];
[super dealloc];
}
- (NSString *) loginAlias
{
return [loginAlias length] ? loginAlias : self->login;
}
- (void) setPrimaryRoles: (NSArray *) newRoles
{
ASSIGN (roles, newRoles);
+2
View File
@@ -44,6 +44,8 @@
@class SOGoContactFolders;
@interface SOGoUserFolder : SOGoFolder
{
}
/* ownership */
+4 -1
View File
@@ -102,11 +102,14 @@
ownerUser = [SOGoUser userWithLogin: nameInContainer roles: nil];
login = [ownerUser login];
[self setOwner: login];
if (![nameInContainer isEqualToString: login])
if (![nameInContainer isEqualToString: login]
&& [nameInContainer rangeOfString: @"!"].location == NSNotFound)
// In case the user domain is specified in the URL but not in the user
// login name, we remove it (user@domain => user).
// This happens when SOGoLoginDomains is defined but
// SOGoEnableDomainBasedUID is disabled.
// The !book alias used by the per-addressbook CardDAV profile must be
// preserved here so generated hrefs keep the discriminator.
ASSIGN(nameInContainer, login);
}