Import Outlook Contacts into your sipgate address book

Robert
19.04.2021 0 13:34 min

What is sipgate.io?

sipgate.io is a collection of APIs, which enables sipgate’s customers to build flexible integrations matching their individual needs.
Among other things, it provides interfaces for sending and receiving text messages or faxes, monitoring the call history, as well as initiating and manipulating calls.
In this tutorial, we will use sipgate.io’s contact API to import contacts from your company’s Microsoft Outlook address book.

In this tutorial

We explain how Outlook contacts can be exported from your Microsoft account and imported into your sipgate account.

For this tutorial, we’ll focus on a one-way sync that imports and updates contacts from Outlook to sipgate.
The reverse can be implemented similarly.

The Microsoft Graph-API is used to fetch all existing contacts which can then be mapped to the format required by sipgate. Afterward, those contacts are imported into sipgate using the sipgate.io REST Api.

Prerequisites: You have node.js and NPM installed on your computer.

Initial sipgate.io setup and contacts API

We’ll use Node.js in order to use the official sipgate.io node library.

First, a new Node.js project is created, containing a sipgate.js file to keep our code to manage contacts with sipgateio.

npm init -y
touch sipgate.js

We’ll also install the sipgate.io library using

npm install sipgateio

In this section, we want to show you how to authenticate your sipgate account and how the sipgate contacts data format looks like.

First of all, we’ll use a personal access token to authenticate to our API.
We can create a personal access token inside the sipgate web interface. For more detailed information on how to create such a token, take a look at our documentation.

The credentials are passed to the script using environment variables and will be stored in a .env file. Take a look at the .env.example template and adjust it to your needs.

Using these variables we create the sipgate.io client and a contacts module.

const { sipgateIO, createContactsModule } = require("sipgateio");

const tokenId = process.env.SIPGATE_TOKEN_ID;
const token = process.env.SIPGATE_TOKEN;

if (!tokenId || !token) {
  throw Error("Please provide a valid sipgate TokenID and Token.");
}

const client = sipgateIO({ tokenId, token });
const contactsModule = createContactsModule(client);

Use the contactsModule.get function to check the connection to the API:

async function run() {
  const contacts = await contactsModule.get("SHARED");
  console.log(contacts);
}

run().catch(console.error);

The output should contain contacts like this one:

{
  "id": "e93f8d65-7018-4a71-9815-987bae5579a8",
  "name": "Ada Lovelace",
  "emails": [
    {
      "email": "ada.lovelace@example.com",
      "type": []
    }
  ],
  "numbers": [
    {
      "number": "+97453132649",
      "type": ["cell"]
    }
  ],
  "addresses": [
    {
      "poBox": null,
      "extendedAddress": null,
      "streetAddress": "Some Address 42",
      "locality": "Berlin",
      "region": "Berlin",
      "postalCode": "10115",
      "country": "Deutschland"
    }
  ],
  "scope": "SHARED"
}

Registering our application at Microsoft

Microsoft’s APIs, like the Microsoft Graph API, are collected in their Azure portal.
There are application has to be registered, so that authentification can take place via OAuth.

Create an Azure App and fill in your credentials:

  1. Log in to Microsoft Azure
  2. Navigate to Azure Active Directory
  3. On the left side click on App registration
  4. Register a new app
  5. The redirect URI must be set to where the service will be available to permit its usage in the OAuth flow. Our example project sets it to http://localhost:3000/auth/callback.
  6. On the left side click on API-Permission and add the OrgContact.Read.All permission.

Now we need to define some more environment variables which can be found in your Azure web interface:

  1. AZURE_OAUTH_REDIRECT_URI: This link will be used to redirect us to the localhost as part of the Azure OAuth process. We use http://localhost:3000/auth/callback as shown in 5..
  2. AZURE_APP_SECRET: In the Azure web interface click on „Certificates and secrets“ on the left side and create a new client secret. Copy the value shown after creating the secret.
  3. AZURE_APP_ID: The Azure Application (client) ID can be taken directly from your Azure app overview.
  4. AZURE_AUTHORITY: The authority should be „https://login.microsoftonline.com/{TENANT_ID}/“. Replace {TENANT_ID} in the URL with your tenant id which can be found under Properties in the Azure web interface. This link is used to authorize with your Outlook account.

Microsoft Graph-API

To use the Microsoft Graph-API we first need to retrieve a valid token via OAuth.

A quick recap of the OAuth flow:

  1. Create a URL that specifies the request scope, client id, and the redirect URI similar to this: https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=…&redirect_uri=…&response_type=code&scope=…
    When you open it, Microsoft will present a login window that notifies you about the permissions given to the app.
  2. Login with your Microsoft account.
  3. On successful authentication Microsoft redirects you to your specified redirect URI with a code query parameter.
  4. Using that code you can make another API request to get the access token.

We use express to run a small server that will receive the OAuth code:

npm install express

We start the express server and listen to the path specified in 1. to handle the OAuth flow explained above.
We save the token to token.json and access it on later occasions if it is still valid.
If the token in token.json is expired we use the method to retrieve a new one.

function authenticateOutlook(callback) {
  const app = express();
  app.get("/auth/callback", async (req, res) => {
    const code = req.query.code;

    /* ... */

    // fetch the token using the `code`
    // save the token in a local `token.json` file
    // close the server

    callback(token);
  });

  console.log(
    "Please open the URL `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=...&redirect_uri=...&response_type=code&scope=...`"
  );

  app.listen(3000);
}

You can see the complete implementation here.

Using that token, we now can access the Outlook API to get the contacts.

Connection between Outlook and sipgate

The first step in the importing process is the download of all private contacts from the Outlook contact.
To do this, Microsoft provides a Microsoft Graph API route: /me/contacts.

For clarity, we create a new class OutlookClient in a separate file. This class encapsulates client functionality for the Microsoft Graph API.

To facilitate the requests, we create an AxiosClient, which has been configured with the corresponding authorization token and Microsoft Graph base-URL.

const axios = require("axios");

const baseURL = "https://graph.microsoft.com/v1.0/";

const authority = process.env.AZURE_AUTHORITY;

class OutlookClient {
  constructor(accessToken) {
    this.token = accessToken;
    this.axios = axios.create({
      baseURL,
      headers: { Authorization: `Bearer ${this.token}` },
    });
  }
}

module.exports = {
  OutlookClient,
};

Contacts that are shared in a user’s company can be acquired by calling the /contacts route.
The relevant contact data is contained in a response object and has to be accessed with .data.value.

  async getAllOrgContacts() {
    return (await this.axios.get(`/contacts`)).data.value;

In our app.js file we can now use this client to get our contacts from Outlook:

let outlookClient = new OutlookClient(accessToken);

let outlookContacts = await outlookClient.getAllOrgContacts();

After a successful request using getAllOrgContacts() method, we should now have a complete array of our Outlook contacts.

Convert Outlook contacts to sipgate contacts

We convert Outlook contacts to sipgate contacts by implementing two functions that make use of Outlook contact and Outlook address objects, the outlookOrgContactToSipgateContact() as well as the outlookAddressToSipgateAddress() functions.

As discussed in the previous section, Outlook contacts possess their own mapping for home, mobile and business numbers.
Phone numbers on the sipgate side have built-in types which have to be mapped to the corresponding number.
We achieve this by translating the OutlookContact values to number objects containing the number and one of the corresponding sipgate number types TYPE_MOBILE, TYPE_HOME or TYPE_WORK using the OutlookNumberTypeToSipgateNumberType() function.

function outlookOrgContactToSipgateContact(outlookContact) {
  const outlookNumberTypeToSipgateNumberType = (type) => {
    if (type === "home") return TYPE_HOME;
    if (type === "business") return TYPE_WORK;
    if (type === "mobile") return TYPE_MOBILE;
    return TYPE_OTHER;
  };
  const numbers = outlookOrgContact.phones
    .filter(({ number, type }) => number !== null)
    .map(({ number, type }) => ({
      number,
      type: [outlookNumberTypeToSipgateNumberType(type)],
    }));
  // ...
}

Similar to phone numbers email addresses also have their own types, but as we will only make use of SHARED contacts here, so we will leave these types empty in this case. We get the mail address from outlookOrgContact.mail and hand it to the new email key in our emails.

function outlookOrgContactToSipgateContact(outlookOrgContact) {
  // ...
  const emails = outlookOrgContact.mail
    ? [{ email: outlookOrgContact.mail, type: [] }]
    : [];
  // ...
}

Hereafter the only values remaining are organization and addresses. While we can easily obtain the organization name by reading the companyName and department values from our Outlook contact object, the addresses will need to be mapped corresponding to the new sipgate format using outlookAddressToSipgateAddress().

function outlookOrgContactToSipgateContact(outlookOrgContact) {
  // ...
  const organization = [
    [outlookContact.companyName || "", outlookContact.department || ""],
  ];
  const addresses = outlookOrgContact.addresses.map(
    outlookAddressToSipgateAddress
  );
  // ...
}

function outlookAddressToSipgateAddress(outlookAddress) {
  return {
    streetAddress: outlookAddress.street,
    postalCode: outlookAddress.postalCode,
    locality: outlookAddress.city,
    region: outlookAddress.state,
    country: outlookAddress.countryOrRegion,
  };
}

Finally, we have all values needed to return a converted sipgate contact, only being short on the name which we can get from outlookOrgContact.displayName.

🗒 Note: We only want to create SHARED and not PRIVATE contacts. This is why we directly pass SHARED as contact scope.

function outlookOrgContactToSipgateContact(outlookOrgContact) {
  // ...
  return {
    name: outlookOrgContact.displayName,
    numbers,
    emails,
    organization,
    addresses,
    scope: "SHARED",
  };
}

To make sure we do not create duplicates or update unchanged contacts, we track them using the Outlook id and a UUID which we create in createNewContact().

🗒 Note: We store these id mappings in the mapping.json.

const createNewContact = async (sipgateContact) => {
  const id = uuid.v4();
  await updateContact(id, sipgateContact);
  return id;
};

Keeping track of imported contacts

To prevent our application from creating new contacts whenever it is run, we need to keep track of the contacts it already has imported to sipgate. To do this, we can simply map the IDs of our Outlook contacts to the IDs of our sipgate contacts. This mapping is then serialized to a JSON file (mapping.json), which is read during program startup.

let mapping = {};
if (await fileExists("mapping.json")) {
  const fileContents = await readFile("mapping.json");
  mapping = JSON.parse(fileContents);
}

To update our list of mappings we simply write the updated object back to the mapping.json file.

await writeFile("mapping.json", JSON.stringify(mapping, null, 4));

Importing contacts

For information purposes we declare two new variables to track the number of created and updated contacts:

let nContactsUpdated = 0;
let nContactsImported = 0;

Now, we can start importing our contacts into sipgate. Instead of iterating through the list of contacts, we can parallelize the process by calling an asynchronous function for each contact using the map method.

const promises = outlookContacts.map(async (outlookContact) => {

First, we convert the outlook contact into a sipgate contact using our conversion function.

    const sipgateContact = conversion.outlookOrgContactToSipgateContact(outlookContact);
      outlookContact
    );

Next, we check if the contact exists among our already imported contacts. If it exists we update the contact using the updateContact method of our sipgate client. We then increment the nContactsUpdated counter by one.

if (outlookContact.id in mapping) {
  let sipgateId = mapping[outlookContact.id];
  console.log(`Contact already exists: ${outlookContact.displayName}`);
  try {
    await sipgate.updateContact(sipgateId, sipgateContact);
  } catch (error) {
    console.log(
      `failed to update contact ${sipgateContact.name}: ${error.message}`
    );
  }

  nContactsUpdated += 1;
}

If the contact does not exist in the mapping of our already imported contacts, we create a new contact using the createNewContact method and the increment nContactsImported counter.

  else {
      console.log(
        `Importing new Outlook contact: ${outlookContact.displayName}`
      );
      let sipgateId = await sipgate.createNewContact(sipgateContact);

      mapping[outlookContact.id] = sipgateId;
      nContactsImported += 1;
    }
  });

To halt program execution until all promises are properly resolved, we call the all function with our list of promises.

await Promise.all(promises);

Your Outlook contacts should now be imported into your sipgate contact book! Lastly, we emit the number of imported and updated contacts and update our mapped contacts by writing the mapping object back to the mappings.json file.

console.log();
console.log(`${nContactsImported} contacts were imported.`);
console.log(`${nContactsUpdated} contacts already existed, updated them.`);

await writeFile("mapping.json", JSON.stringify(mapping, null, 4));

Running the script

To run the script simply place a file called .env containing your Azure and sipgate credentials in the same directory as your app.js and execute it with the following command:

node app.js

Delete shared contacts from your sipgate contacts

⚠️ WARNING: This will delete all your shared sipgate contacts. Use only with caution!

If you want to clear your shared contacts before importing from Outlook, you can do this by deleting the mapping.json file and calling the /contacts endpoint of the sipgate.io REST API.

Add the following function in your sipgate.js file:

const deleteAllSharedContacts = async () => {
  const sharedContacts = await contactsModule.get("SHARED");
  console.log("Deleting all shared contacts.");

  const promises = sharedContacts.map((contact) =>
    client
      .delete(`/contacts/${contact.id}`)
      .then(() =>
        console.log(`Deleting contact: ${contact.name} (${contact.id}).`)
      )
  );
  await Promise.all(promises);
};

We retrieve all shared contacts from our sipgate contacts and then call a DELETE request for each of them using their contact ID as a URL parameter. Again, we use the asynchronous map technique that we used earlier to run the API requests asynchronously.

Now that we have extended our sipgate.js file, let’s use it in a new script called clean-sipgate-contacts.js.

require("dotenv").config();

const sipgate = require("./sipgate");

const fs = require("fs");
const util = require("util");

const deleteFile = util.promisify(fs.unlink);
const fileExists = util.promisify(fs.exists);

async function run() {
  if (await fileExists("./mapping.json")) {
    console.log("Deleting mapping.json.");
    await deleteFile("./mapping.json");
  }
  await sipgate.deleteAllSharedContacts();
}

run().catch(console.error);

The script is really simple. We just import our sipgate.js module, call its deleteAllSharedContacts() function and delete the mapping.json file using a promisified version of the fs.unlink and fs.exists functions.

Automate contact import using a cronjob

You can automate the contact import by adding a cronjob. To run it every day at midnight add the following line to your system’s crontab:

0 0 * * * <user> /usr/bin/node /path/to/your/app.js

Conclusion

If you’ve followed along until now, congratulations! In this tutorial, you have learned how to do a basic import of company Outlook contacts into your sipgate contacts.
It can easily be extended to include personal Outlook contacts or contacts from another source.

The full project can be found at our GitHub repository.

To explore more capabilities of our sipgate.io library, check out our other tutorials e.g. about creating insightful live statistics.

Keine Kommentare


Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.