I am trying to access my iCloud contacts in my UWP application.
I know that I can access my Gmail Contacts through Google's People API and my Outlook contacts through Microsoft's Graph api OR Outook people api .
Does Apple provide an API (Rest or otherwise) that could be leveraged to fetch, update, add, delete contacts? If yes, is there a tutorial that walks through how to setup access iCloud api?
So, if you just want to fetch the contacts (and in JSON format) my previous answer is probably the way to go.
However, as I wanted to be able to perform CRUD operations on Apple account, a better way is to use the CARDDAV protocol that icloud/apple supports.
- Get the principle user using basic authentication (the password used in this basic authentication would have to be generated as an app password here) by firing a PROPFIND request at https://contacts.icloud.com with the request content:
<propfind
xmlns="DAV:">
<prop>
<current-user-principal/>
</prop>
</propfind>
- The previous step will let you retrieve a principle which will be of the format /1437425399/principal/. Now, you can fire a PROPFIND query the home-set for this user at the link https://contacts.icloud.com/1437425399/principal with the following request content:
<propfind
xmlns="DAV:"
xmlns:c="urn:ietf:params:xml:ns:carddav">
<prop>
<c:addressbook-home-set/>
</prop>
</propfind>
- From the previous request, you will get the link to the homeset in the format https://p48-contacts.icloud.com:443/1437425399/carddavhome/. You can query where the vcards for the user exist using the following PROPFIND request at the home-set link:
<propfind
xmlns="DAV:">
<prop>
<resourcetype/>
</prop>
</propfind>
- You will receive the place where all the cards are placed (eg. /1437425399/carddavhome/card/). Using a addressbook query, now you can initiate a REPORT request at the endpoint received in the previous link:
<c:addressbook-query
xmlns="DAV:"
xmlns:c="urn:ietf:params:xml:ns:carddav">
<prop>
</prop>
</c:addressbook-query>
- This will give you all the cards in the tag. You can then fetch multiple cards using a addressbook-multiget REPORT request:
<c:addressbook-multiget
xmlns="DAV:"
xmlns:c="urn:ietf:params:xml:ns:carddav">
<prop>
<getetag />
<c:address-data />
</prop>
<href>/1437425399/carddavhome/card/somecard.vcf</href>
<href>/1437425399/carddavhome/card/anothercard.vcf</href>
</c:addressbook-multiget>
For updating, creating and deleting cards, you can fire PUT requests at "/1437425399/carddavhome/card/cardtobemanipulated.vcf" endpoint using the basic authentication that we discussed earlier after setting the Content-Type as "text/vcard" and sending the VCard in the content for update and create requests.
After spending many days trying to figure this out, I think I am starting to crack this. I am not sure (may be I am) if Apple intentionally makes this difficult but this is more difficult than it should be.
Here is what I had to do to even fetch the contacts (have no idea how to add, update, delete them at all):
Step 1> Request a token using apple id and password. If 2FA is activated, you will not receive the auth token and auth user cookies in the response header. But that is okay, we will get to that in the next step. Note that most probably the extraction of the AUTH-TOKEN and AUTH-USER will fail for now. Also, we will receive the locale information and the contact link in the response which we will need later on.
public sealed partial class MainPage : Page
{
String contactLink = "";
String authToken = "";
String authUser = "";
public MainPage()
{
this.InitializeComponent();
}
private async void IcloudBtn_Click(object sender, RoutedEventArgs e)
{
HttpResponseHeaderCollection respHeaders = await ContactApple();
ContactsBlock.Text += "CONTACT LINK: " + contactLink + "\n";
ContactsBlock.Text += authToken + "\n";
ContactsBlock.Text += authUser + "\n";
}
private async void GetToken_Click(object sender, RoutedEventArgs e)
{
HttpResponseHeaderCollection respHeaders = await ContactApple(AuthenticationCode.Text);
ContactsBlock.Text += "CONTACT LINK: " + contactLink + "\n";
ContactsBlock.Text += authToken + "\n";
ContactsBlock.Text += authUser + "\n";
}
private async void GetContacts_Click(object sender, RoutedEventArgs e)
{
Uri contactUri = new Uri(contactLink + "/co/startup?locale=en_US&order=last%2Cfirst");
HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Add("Origin", "https://www.icloud.com");
client.DefaultRequestHeaders.Add("Cookie", authToken + ";" + authUser + ";");
HttpResponseMessage hrm = await client.GetAsync(contactUri);
JObject contactJson = JObject.Parse(await hrm.Content.ReadAsStringAsync());
ContactsBlock.Text += contactJson["contacts"];
}
private async Task<HttpResponseHeaderCollection> ContactApple(String authentication = "")
{
// The apple id and the password
String appleId = "AppleID";
String password = "Password" + authentication;
// https://stackoverflow.com/questions/31457068/get-icloud-contact-list-in-c-sharp?noredirect=1&lq=1
// Post request will have this in the content
String data = "{\"apple_id\":" + appleId + ", \"password\":" + password + ", \"extended_login\":true}";
HttpStringContent content = new HttpStringContent(data, UnicodeEncoding.Utf8);
// The URI to get the tokens from:
Uri requestUri = new Uri("https://setup.icloud.com/setup/ws/1/accountLogin");
// Create an instance of the HttpClient (Windows.Web.Http)
HttpClient client = new HttpClient();
// Add Origin = https://www.icloud.com in the header.
client.DefaultRequestHeaders.Add("Origin", "https://www.icloud.com");
// Post request and read response as JSON object (NewtonSoft)
HttpResponseMessage hrm = await client.PostAsync(requestUri, content);
JObject resp = JObject.Parse(await hrm.Content.ReadAsStringAsync());
// Get the URL to the contacts
contactLink = (String)resp["webservices"]["contacts"]["url"];
// Read the headers for AUTH-TOKEN and AUTH-USER Cookies,
HttpResponseHeaderCollection headers = hrm.Headers;
if (headers.ContainsKey("Set-Cookie"))
{
String cookie = headers["Set-Cookie"];
char[] separators = { ';', ',' };
String[] tokens = cookie.Split(separators);
foreach (String token in tokens)
{
int length = token.Length;
if (token.Contains("X-APPLE-WEBAUTH-TOKEN"))
{
authToken = token;
}
if (token.Contains("X-APPLE-WEBAUTH-USER"))
{
authUser = token;
}
}
}
return headers;
}
}
Step 2> If 2FA is activated, you will receive a request on your Apple Device to authorize the login and to use the provided code for logging in. Once you have the code, then repeat the same step as above only append the code to your password by providing in the authorization code text field.
Note that you will need to enter the authorization code.
Step 3> Hopefully, you would have now received tokens necessary for fetching the contacts. Now, you can fetch the contacts by using the contact link obtained in the response body while fetching the tokens and appending "/co/startup?locale=XXXXX&order=last%2Cfirst"
Note that you will have to use the locale that suits your needs. I needed to use "en_US" because that is what I had in my locale in the response. You will need to use the locale that is returned in your request.
XAML:
<Page
x:Class="AddressBook.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:AddressBook"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<StackPanel x:Name="MainStack">
<Button x:Name="IcloudBtn" Content="Request Code" Click="IcloudBtn_Click"/>
<TextBox x:Name="AuthenticationCode" Text="Authentication Code here." TextWrapping="Wrap"/>
<Button x:Name="GetToken" Content="Get Token" Click="GetToken_Click"/>
<Button x:Name="GetContacts" Content="Get Contacts" Click="GetContacts_Click"/>
<TextBox x:Name="ContactsBlock" TextWrapping="Wrap"/>
</StackPanel>
</Page>
However, I still have no idea how to actually manipulate the list of contacts. I guess if I find out, I will update here.