Zoiper SIP Client

SIP-based softphone client for voice and video calls over IP.

Zoiper is a cross-platform softphone client that supports SIP and IAX protocols, enabling users to make voice and video calls over the internet.
It’s available for macOS, Windows, Linux, iOS, and Android.
Zoiper is compatible with most VoIP providers and PBX systems.

Key Features:

  • Supports SIP and IAX protocols
  • Works with audio and video calls
  • Secure with TLS, SRTP, and ZRTP encryption
  • Compatible with various codecs (G.729, Opus, G.711, etc.)
  • Contact integration and click-to-dial support (with Chrome extension)
  • Free and paid versions (Pro adds video, encryption, and more codecs)

Setup only involves entering SIP credentials provided by your VoIP or PBX provider. See SIP / VoiP

User Manual: https://www.zoiper.com/pdf/User%20Guide%20Zoiper%205%20v.1.0.7.pdf

24 Mar 2025

Started (re)using.

XML Contact Service

Zoiper can read from an XML file to populate the contact list.

So I will generate automatically an XML file from my contacts database, on a daily basis (or manually if needed).

The great thing too is that I can generate many XML versions, and use each one of them in Zoiper as a separate contact list.

So e.g. I can have:
- all my contacts
- VIP contacts
- In email sequences
- French contacts
- etc.

XML needs to follow this format:

https://www.zoiper.com/documentation/XML%20Contact%20Service%20in%20Zoiper%205%20PRO.pdf

<?xml version="1.0" encoding="utf-8"?>
<Contacts>
  <Contact id="123">
    <Name>
      <First></First>
      <Middle></Middle>
      <Last></Last>
      <Display></Display>
    </Name>
    <Info>
      <Company></Company>
    </Info>
    <Phone>
      <Type></Type>
      <!--> Possible value one of the following: Work or Home -->
      <Type></Type>
      <!--> Possible value one the following: Phone, Cell, Pager, IPPhone, Mail, Fax, Pager or Custom -->
      <CustomType></CustomType>
      <!--> This is a custom text. It will be used only when “Custom” option is selected in <Type></Type> -->
      <Phone></Phone>
      <!--> The value is the actual phone number of the contact.
           For the contact to show in zoiper5 at least one phone field should be present -->
      <Account></Account>
      <!--> The value is the ident value of the account that will be used for dialing, it is found in the config.xml file
           If not provided Zoiper will use the XML contact service account (by default it’s the default account) -->
      <Presence></Presence>
      <!--> The ident value of the account that will be used for presence, it is found in the config.xml file
           By default it’s do not use unless it is an IPPhone type -->
      <AccountMappingType></AccountMappingType>
      <!--> Possible value one of the following:
           None = do not use
           Default = the default account used by zoiper5
           Service = the account used by XML contact service
           Custom = when this is used, the ident value of the account should be provided in <Account></Account>, it is found in the config.xml -->
      <PresenceMappingType>None/Service/Custom</PresenceMappingType>
      <!--> Possible value one of the following:
           None = do not use
           Service = the account used by XML contact service.
           Custom = when this is used, the ident value of the account should be provided in <Presence></Presence>, it is found in the config.xml -->
    </Phone>
    <!--> If required more than one phone for a contact a new phone tag should be added that includes at least the two types tags and phone tag -->
    <Avatar>
      <URL></URL>
    </Avatar>
  </Contact>
</Contacts>

Code has been optimised to be able to generate multiple XML files, and easily add new query/file pairs.

# Define the queries and their corresponding output files
QUERY_CONFIG = [
    {
        "query": """
            SELECT rowid, domain, first, last, country, title, phone_mobile, phone_hq, phone, company
            FROM people
            WHERE connected IS NOT NULL
        """,
        "output_file": "/Users/xx/xx/xx/zoiper_contacts_all.xml"
    },
    {
        "query": """
            SELECT rowid, domain, first, last, country, title, phone_mobile, phone_hq, phone, company
            FROM people
            WHERE connected IS NOT NULL
            AND nicai = 1
        """,
        "output_file": "/Users/xx/xx/xx/zoiper_contacts_nicai.xml"
    }
]

# FUNCTIONS

def prettify(elem):
    """Return a pretty-printed XML string for the Element."""
    rough_string = ET.tostring(elem, 'utf-8')
    reparsed = minidom.parseString(rough_string)
    return reparsed.toprettyxml(indent="  ")

def create_contacts_xml(cursor, query, output_file):
    """Generate XML file for contacts based on query results."""
    cursor.execute(query)
    rows = cursor.fetchall()
    count_total = len(rows)
    count = 0
    count_row = 0

    # Create the root element for the XML
    root = ET.Element("Contacts")

    # Process each row from the database
    for row in rows:
        count_row += 1
        rowid, domain, first, last, country, title, phone_mobile, phone_hq, phone, company = row

        if verbose:
            print(f"Processing {first} {last} from {domain}")

        # Skip if no phone number available
        if not any([phone_mobile, phone_hq, phone]):
            continue

        # Create contact element with ID
        contact = ET.SubElement(root, "Contact")
        contact.set("id", str(rowid))

        # Add name information
        name = ET.SubElement(contact, "Name")
        first_name = ET.SubElement(name, "First")
        first_name.text = first
        middle = ET.SubElement(name, "Middle")
        last_name = ET.SubElement(name, "Last")
        last_name.text = last
        display = ET.SubElement(name, "Display")
        display.text = f"{first} {last} ({title} @ {company})"

        # Add company information
        info = ET.SubElement(contact, "Info")
        company_elem = ET.SubElement(info, "Company")
        company_elem.text = domain

        # Add phone numbers
        for phone_value, phone_type in [
            (phone_mobile, "Cell"),
            (phone_hq, "Phone"),
            (phone, "Phone")
        ]:
            if phone_value and (phone_type != "Phone" or (phone_value != phone_mobile and phone_value != phone_hq)):
                phone_elem = ET.SubElement(contact, "Phone")
                type1 = ET.SubElement(phone_elem, "Type")
                type2 = ET.SubElement(phone_elem, "Type")
                type2.text = phone_type
                phone_number = ET.SubElement(phone_elem, "Phone")
                phone_number.text = phone_value
                account_mapping = ET.SubElement(phone_elem, "AccountMappingType")
                account_mapping.text = "Default"
                presence_mapping = ET.SubElement(phone_elem, "PresenceMappingType")
                presence_mapping.text = "None"
                count += 1

    # Write the XML to a file
    with open(output_file, "w", encoding="utf-8") as f:
        f.write(prettify(root))

    print(f"\n✅ XML file generated: {output_file}")
    return count_total, count_row, count

# MAIN
with sqlite3.connect(DB_BTOB) as conn:
    cur = conn.cursor()

    # Process each query configuration
    for config in QUERY_CONFIG:
        count_total, count_row, count = create_contacts_xml(cur, config["query"], config["output_file"])
        print(f"\nStats for {config['output_file']}:")
        print(f"Total records:\t{count_total:,}")
        print(f"Processed rows:\t{count_row:,}")
        print(f"Phone numbers:\t{count:,}")

links

social