Skip to content

Commit

Permalink
feat: support email in name fields (single and double)
Browse files Browse the repository at this point in the history
  • Loading branch information
mbideau committed May 7, 2023
1 parent 8683c91 commit 734a485
Show file tree
Hide file tree
Showing 13 changed files with 143 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
BEGIN:VCARD
VERSION:3.0
EMAIL:[email protected]
FN:Double Parenthesesemail
N:Parenthesesemail;Double;;;
END:VCARD
6 changes: 6 additions & 0 deletions test/cases/Email in Name/expected/Second Email.vcard
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
BEGIN:VCARD
VERSION:3.0
EMAIL:[email protected]
FN:Second Email
N:Email;Second;;;
END:VCARD
1 change: 1 addition & 0 deletions test/cases/Email in Name/src/Double_Email.v3.0.vcf
6 changes: 6 additions & 0 deletions test/cases/Empty Name/expected/Empty Name.vcard
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
BEGIN:VCARD
VERSION:3.0
EMAIL:[email protected]
FN:Empty Name
N:Name;Empty;;;
END:VCARD
6 changes: 6 additions & 0 deletions test/cases/Empty Name/expected/No Name.vcard
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
BEGIN:VCARD
VERSION:3.0
EMAIL:[email protected]
FN:No Name
N:Name;No;;;
END:VCARD
1 change: 1 addition & 0 deletions test/cases/Empty Name/src/Empty_Name.v3.0.vcf
1 change: 1 addition & 0 deletions test/cases/Empty Name/src/No_Name.v3.0.vcf
6 changes: 6 additions & 0 deletions test/ressources/src/Double_Email.v3.0.vcf
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
BEGIN:VCARD
VERSION:3.0
N:Email;Double;;;
FN:[email protected] <[email protected]>
EMAIL:[email protected]
END:VCARD
6 changes: 6 additions & 0 deletions test/ressources/src/Double_ParenthesesEmail.v3.0.vcf
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
BEGIN:VCARD
VERSION:3.0
N:ParenthesesEmail;Double;;;
FN:[email protected] ([email protected])
EMAIL:[email protected]
END:VCARD
6 changes: 6 additions & 0 deletions test/ressources/src/Empty_Name.v3.0.vcf
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
BEGIN:VCARD
VERSION:3.0
N:Name;Empty;;;
FN:
EMAIL:[email protected]
END:VCARD
6 changes: 6 additions & 0 deletions test/ressources/src/No_Name.v3.0.vcf
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
BEGIN:VCARD
VERSION:3.0
N:;;;;
FN:
EMAIL:[email protected]
END:VCARD
97 changes: 91 additions & 6 deletions vcardlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import warnings
import binascii
from os.path import exists, basename
from email.utils import parseaddr
# @see: https://eventable.github.io/vobject/
from vobject import vCard, readComponents
from vobject.vcard import Name
Expand Down Expand Up @@ -48,6 +49,15 @@
REGEX_ANY_NUMBER = re.compile('[0-9]')
REGEX_WITHOUT_EXTENSION = re.compile('(.+)\\.[a-zA-Z]+$')
REGEX_NAME_IN_EMAIL = re.compile('^ *"(?P<name>[^"]+)" *<[^>]+> *$')
REGEX_EMAIL_SURROUNDINGS = [
re.compile(
'(?P<space_before>\\s*)\\[(?P<local>[^@]]+)@(?P<domain>[^]]+)\\](?P<space_after>\\s*)'),
re.compile(
'(?P<space_before>\\s*)\\((?P<local>[^@)]+)@(?P<domain>[^)]+)\\)(?P<space_after>\\s*)'),
re.compile(
'(?P<space_before>\\s*)"(?P<local>[^@"]+)@(?P<domain>[^"]+)"(?P<space_after>\\s*)'),
re.compile(
'(?P<space_before>\\s*)\'(?P<local>[^@\']+)@(?P<domain>[^\']+)\'(?P<space_after>\\s*)')]
REGEX_EMAIL_WITH_NAME = re.compile('^ *"[^"]+" *<(?P<email>[^>]+)> *$')
REGEX_INVALID_MAIL = re.compile('^nobody[a-z0-9]*@nowhere.invalid$')
REGEX_ONY_NON_ALPHANUM = re.compile('^[ ]*[^\\w]*[ ]*$')
Expand Down Expand Up @@ -531,12 +541,87 @@ def collect_vcard_names(vcard): # pylint: disable=too-many-statements,too-many-
for attr_n in getattr(vcard, name_key + '_list'):
value = close_parentheses_or_braces(str(attr_n.value).strip())
if not REGEX_ONY_NON_ALPHANUM.match(value):
if value.count('@') == 1:
name = build_name_from_email(value)
if not name in available_names:
available_names.append(name)
logging.debug("\t\tadding '%s' from built email for '%s'",
name, name_key)
if '@' in value:
normalized_value = value.strip()
for regex in REGEX_EMAIL_SURROUNDINGS:
normalized_value = regex.sub(
'\\g<space_before><\\g<local>@\\g<domain>>\\g<space_after>',
normalized_value).replace('<<', '<').replace('>>', '>')
logging.debug("\t\tnormalized value '%s' from '%s'",
normalized_value, value)
realname, email = parseaddr(normalized_value)
logging.debug("\t\tparsed email: '%s', '%s' from '%s'",
realname, email, normalized_value)
if realname or email:

# Special case to be able to process value with double email like:
# "[email protected] <[email protected]>"
# In this case, the parsing will return an empty name, and the 1st
# email as email, ignoring the second one.
# In order to force the processing of the second email, we remove
# the 1st email from the original value and re-parse to get the 2nd
# one.
if not realname and email and normalized_value.count('@') == 2:
realname = email
_, email = parseaddr(normalized_value.replace(email, ''))
logging.debug("\t\tforce parsed email: '%s', '%s' from '%s'",
realname, email, normalized_value)

if realname:
if '@' in realname:
normalized_realname = realname.strip()
for regex in REGEX_EMAIL_SURROUNDINGS:
normalized_realname = regex.sub(
'\\g<space_before><\\g<local>@'
'\\g<domain>>\\g<space_after>',
normalized_realname).\
replace('<<', '<').replace('>>', '>')
logging.debug("\t\tnormalized sub-value '%s' from '%s'",
normalized_realname, realname)
_realname, _email = parseaddr(normalized_realname)
logging.debug("\t\tparsed email: '%s', '%s' from '%s'",
_realname, _email, normalized_realname)
if _realname or _email:
if _realname:
name = sanitize_name(_realname)
if not name in available_names:
available_names.append(name)
logging.debug(
"\t\tadding '%s' from built email for '%s'",
name, name_key)
if _email:
name = build_name_from_email(_email)
if not name in available_names:
available_names.append(name)
logging.debug(
"\t\tadding '%s' from built email for '%s'",
name, name_key)
elif normalized_realname.count('@') == 1:
name = build_name_from_email(normalized_realname)
if not name in available_names:
available_names.append(name)
logging.debug(
"\t\tadding '%s' from built email for '%s'",
name, name_key)
else:
logging.debug(
"\t\tcan't parse name value containing email '%s'",
normalized_realname)
if email:
name = build_name_from_email(email)
if not name in available_names:
available_names.append(name)
logging.debug("\t\tadding '%s' from built email for '%s'",
name, name_key)
elif normalized_value.count('@') == 1:
name = build_name_from_email(normalized_value)
if not name in available_names:
available_names.append(name)
logging.debug("\t\tadding '%s' from built email for '%s'",
name, name_key)
else:
logging.debug("\t\tcan't parse name value containing email '%s'",
normalized_value)
else:
name = sanitize_name(value)
if not name in available_names:
Expand Down

0 comments on commit 734a485

Please sign in to comment.