Skip to content

Commit

Permalink
Allow carddav sync during import
Browse files Browse the repository at this point in the history
  • Loading branch information
tonioo committed Dec 12, 2023
1 parent 47ecd03 commit e69814f
Show file tree
Hide file tree
Showing 6 changed files with 85 additions and 16 deletions.
7 changes: 5 additions & 2 deletions modoboa_contacts/importer/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ def detect_import_backend(fp, delimiter: str = ";"):
raise RuntimeError("Failed to detect backend to use")


def import_csv_file(addressbook, csv_filename: str, delimiter: str):
def import_csv_file(addressbook,
csv_filename: str,
delimiter: str,
carddav_password: str = None):
with open(csv_filename) as fp:
backend, rows = detect_import_backend(fp, delimiter)
backend(addressbook).proceed(rows)
backend(addressbook).proceed(rows, carddav_password)
18 changes: 15 additions & 3 deletions modoboa_contacts/importer/backends/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from modoboa_contacts import models
from modoboa_contacts import models, tasks


class ImporterBackend:
Expand Down Expand Up @@ -41,6 +41,18 @@ def import_contact(self, row) -> models.Contact:
)
return contact

def proceed(self, rows: list):
def proceed(self, rows: list, carddav_password: str = None):
for row in rows:
self.import_contact(row)
contact = self.import_contact(row)
if carddav_password:
# FIXME: refactor CDAV tasks to allow connection from
# credentials and not only request
clt = tasks.get_cdav_client(
self.addressbook,
self.addressbook.user.email,
carddav_password,
True
)
path, etag = clt.upload_new_card(contact.uid, contact.to_vcard())
contact.etag = etag
contact.save(update_fields=["etag"])
14 changes: 13 additions & 1 deletion modoboa_contacts/management/commands/import_contacts.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ def add_arguments(self, parser):
"--delimiter", type=str, default=",",
help="Delimiter used in CSV file"
)
parser.add_argument(
"--carddav-password", type=str, default=None,
help=(
"Password associated to email. If provided, imported "
"contacts will be synced to CardDAV servert too"
)
)
parser.add_argument(
"email", type=str,
help="Email address to import contacts for"
Expand All @@ -35,7 +42,12 @@ def handle(self, *args, **options):
"Address Book for email '%s' not found" % options["email"]
)
try:
import_csv_file(addressbook, options["file"], options["delimiter"])
import_csv_file(
addressbook,
options["file"],
options["delimiter"],
options.get("carddav_password")
)
except RuntimeError as err:
raise CommandError(err)
self.stdout.write(
Expand Down
27 changes: 19 additions & 8 deletions modoboa_contacts/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,26 @@
from . import models


def get_cdav_client(request, addressbook, write_support=False):
def get_cdav_client(addressbook, user: str, passwd: str, write_support=False):
"""Instantiate a new CardDAV client."""
return carddav.PyCardDAV(
addressbook.url, user=request.user.username,
passwd=cryptutils.decrypt(request.session["password"]),
addressbook.url,
user=user,
passwd=passwd,
write_support=write_support
)


def get_cdav_client_from_request(request, addressbook, *args, **kwargs):
"""Create a connection from a Request object."""
return get_cdav_client(
addressbook,
request.user.username,
passwd=cryptutils.decrypt(request.session["password"]),
**kwargs
)


def create_cdav_addressbook(addressbook, password):
"""Create CardDAV address book."""
clt = carddav.PyCardDAV(
Expand All @@ -32,7 +43,7 @@ def push_addressbook_to_carddav(request, addressbook):
Use only once.
"""
clt = get_cdav_client(request, addressbook, True)
clt = get_cdav_client_from_request(request, addressbook, write_support=True)
for contact in addressbook.contact_set.all():
href, etag = clt.upload_new_card(contact.uid, contact.to_vcard())
contact.etag = etag
Expand All @@ -44,7 +55,7 @@ def push_addressbook_to_carddav(request, addressbook):

def sync_addressbook_from_cdav(request, addressbook):
"""Fetch changes from CardDAV server."""
clt = get_cdav_client(request, addressbook)
clt = get_cdav_client_from_request(request, addressbook)
changes = clt.sync_vcards(addressbook.sync_token)
if not len(changes["cards"]):
return
Expand All @@ -71,15 +82,15 @@ def sync_addressbook_from_cdav(request, addressbook):

def push_contact_to_cdav(request, contact):
"""Upload new contact to cdav collection."""
clt = get_cdav_client(request, contact.addressbook, True)
clt = get_cdav_client_from_request(request, contact.addressbook, write_support=True)
path, etag = clt.upload_new_card(contact.uid, contact.to_vcard())
contact.etag = etag
contact.save(update_fields=["etag"])


def update_contact_cdav(request, contact):
"""Update existing contact."""
clt = get_cdav_client(request, contact.addressbook, True)
clt = get_cdav_client_from_request(request, contact.addressbook, write_support=True)
uid = contact.uid
if not uid.endswith(".vcf"):
uid += ".vcf"
Expand All @@ -90,7 +101,7 @@ def update_contact_cdav(request, contact):

def delete_contact_cdav(request, contact):
"""Delete a contact."""
clt = get_cdav_client(request, contact.addressbook, True)
clt = get_cdav_client_from_request(request, contact.addressbook, write_support=True)
uid = contact.uid
if not uid.endswith(".vcf"):
uid += ".vcf"
Expand Down
2 changes: 2 additions & 0 deletions modoboa_contacts/test_data/unknown_export.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
"First Name","Middle Name","Last Name","Company","E-mail Address","Business Phone","Business Street","Business Street 2","City","Business State","Business Postal Code"
Toto,Tata,Titi,Company,[email protected],12345678,Street 1,Street 2,City,State,France
33 changes: 31 additions & 2 deletions modoboa_contacts/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -351,17 +351,29 @@ def test_emails_list(self):
class ImportTestCase(TestDataMixin, ModoTestCase):

def setUp(self):
super().setUp()
self.path = os.path.join(
os.path.abspath(os.path.dirname(__file__)),
"test_data/outlook_export.csv"
)
self.wrong_path = os.path.join(
os.path.abspath(os.path.dirname(__file__)),
"test_data/unknown_export.csv"
)

def test_import_wrong_addressbook(self):
with self.assertRaises(management.base.CommandError) as ctx:
management.call_command(
"import_contacts", "[email protected]", self.path)
self.assertEqual(ctx.exception,
"Address Book for email '[email protected]' not found")
self.assertEqual(str(ctx.exception),
"Address Book for email '[email protected]' not found")

def test_import_unknown_backend(self):
with self.assertRaises(management.base.CommandError) as ctx:
management.call_command(
"import_contacts", "[email protected]", self.wrong_path)
self.assertEqual(str(ctx.exception),
"Failed to detect backend to use")

def test_import_from_outlook(self):
management.call_command(
Expand All @@ -376,3 +388,20 @@ def test_import_from_outlook(self):
address.contact.address,
"Street 1 Street 2"
)

def test_import_and_carddav_sync(self):
with httmock.HTTMock(mocks.options_mock, mocks.put_mock):
management.call_command(
"import_contacts", "[email protected]", self.path,
carddav_password="Toto1234"
)
address = models.EmailAddress.objects.get(
address="[email protected]")
phone = models.PhoneNumber.objects.get(
number="12345678")
self.assertEqual(address.contact.first_name, "Toto Tata")
self.assertEqual(address.contact.addressbook.user.email, "[email protected]")
self.assertEqual(
address.contact.address,
"Street 1 Street 2"
)

0 comments on commit e69814f

Please sign in to comment.