GithubHelp home page GithubHelp logo

icloud-api's Introduction

iCloud API

This is an unofficial iCloud API that provides useful methods to interact with some services of iCloud.

Attention

Please make sure that this API is my private work and therefore it will never be supported by Apple. iCloud's endpoints and functionality can be changed every time by Apple. Using this project is on your risk and it's possible that apple bans your account. They can do whatever they want and because of the fact that it doesn't fits in to Apples agreements, I can not ensure using this API without risks.

The API's I've implemented are more less the same as Apple provides. Sometimes, the objects are simplified or a little bit restructured because the requirements of such a Javascript API are different from the original API Apple provides. But all in all it's Apple's data and I'm not the author of the idea how an API's result works in detail.

I can not be sure that Apple does not provide any kind of changes in their API. That means, it is possible that something does not work as expected although you did it as it is described in the Documentation here.

Installation

npm install apple-icloud

Getting Started

// Require the module
const iCloud = require('apple-icloud');

var session = {}; // An empty session. Has to be a session object or file path that points on a JSON file containing your session
var username = "[email protected]"; // Your apple id
var password = "totally-safe-password"; // Your password

// This creates your iCloud instance
var myCloud = new iCloud(session, username, password);

myCloud.on("ready", function() {
  // This event fires when your session is ready to use. If you used a valid session as argument, it will be fired directly after calling 'new iCloud()' but sometimes your session is invalid and the constructor has to re-login your account. In this case this event will be fired after the time that is needed to login your account.
});

Session broken

Basically, your instance always tries to use an existing session. You can pass a session by an object or a file path that points to a JSON file. If the passed session is invalid because of any reason, your instance will use your given credentials to re-login. If you did not gave any credentials to the constructor, your instance will try to get the credentials from the given (and obviously broken) session that are normally stored within.

// This creates your iCloud instance
var myCloud = new iCloud("path/to/my/session.json", username, password);

This way is more practicable because it does a lot of stuff automatically. E.g. if your session is too old or invalid, this method just creates a new one. If you use this way of logging in, you will be save that everything works fine. Therefore that is the recommended way.

Now you have a working session and you should continue at API

Instance Call

I already explained that the way I use, is the recommended one but theoretically you can do everything manually.

The arguments I use here are all optional but if you don't use them you have to initialize your instance manually. That is possible but not easy as the way above with only three arguments.

Detailed Way (Not Recommended)

Login manually

As I already explained, you can do everything manually.

// This creates your iCloud instance without any arguments
var myCloud = new iCloud();

// This creates a new session with your credentials. This is very slow if you do it every time and also not recommended because apple bans your account if you login too often in a small time area
myCloud.login(username, password, function(err) {
  if (err) {
    // An error occured
  }
  // You logged in successfully!
});

This is maybe interesting if you have an existing session but you want to re-login your account because of some reason I don't know. But please keep in mind that you really should use existing sessions much as possible because you save a lot of time and avoid being banned by Apple.

Export Session

If you have an existing session, you can export it just calling exportSession() in iCloud instance. This returns a session object that can be loaded the next time you create an iCloud instance new iCloud(session, username, password).

var newSession = myCloud.exportSession();

Save Session

Your iCloud instance (myCloud) also contains a method called saveSession that saves the session directly to a file path.

myCloud.saveSession("/path/to/my/session.json");

If you don't put a file path as argument, the method will use the path you used with new iCloud(path, username, password). If you did not used such a file path there, an error occurs. In practise, that means you can do the following successfully:

var myCloud = new iCloud("path/to/my/session.json", username, password);

/*
  Do your cool stuff.
*/

// Saves session to 'path/to/my/session.json' because your instance knows this path already and you used no custom one as argument
myCloud.saveSession();

Save Session automatically

To save the session automatically when something changes, just listen to the sessionUpdate event and apply saveSession().

myCloud.on("sessionUpdate", function() {
  myCloud.saveSession();
});

IMPORTANT

Please. Save your sessions and use them often as possible. This avoids problems with the API. If you do not use session saving, your client has to do the complete login process every time you start your script. This takes a lot of time and destroys functionality on Apples's servers because they may stop sending important cookies. Therefore: Just do it.

Two Factor Authentication

If the two-factor-authentication is required, your login works basically as expected. You get the ready event on your instance and so on. But you cannot use the most API's such as Contacts, Mail, Notes and so on unless you type in the security code. The only API's that are available are the ones that are also available on iCloud.com without typing in a security code, such as FindMe.

Check for Two Factor Authentication

To check wether the two-factor-authentication is required, you can do 2 things.

  1. Listen to the err event and check the error with the error code 15 and the message Two-factor-authentication is required..
  2. Check the boolean variable twoFactorAuthenticationIsRequired of your iCloud instance.
// ... Logged in successfully

const needsSecurityCode = myCloud.twoFactorAuthenticationIsRequired;

if (needsSecurityCode) {
  console.error("Two Factor Authentication is required. Type in your security code!");
}
else {
  console.log("Everything okay. Go on!");
}

Type in Security Code

To enable the other API's, you need to type in a security code that will be displayed on your devices.

// Sample code '123456'
myCloud.securityCode = "123456";

// When finished, a `ready` event will be triggered.
myCloud.on("ready", function() {
  // Ready event
});

Of course, the ready event now will fire the second time because it already fired after first login (This returned the requirement of two-factor-authentication). To check wether the code is typed in now, look up the twoFactorAuthenticationIsRequired property that returns wether you need the security code (Should be false after you passed in a security code, otherwise the code seems to be invalid).

Example

Everything sounds complicated?

No problem, just look at this short example for a login that supports two-factor-authentication. (Please keep in mind, that I am using prompt here, to get user's input. Of course, you can get your security code as you want).

// ...
// 'username' & 'password' are defined here...

// This creates your iCloud instance
var myCloud = new iCloud("icloud-session.json", username, password);

myCloud.on("ready", function() {
  console.log("Ready Event!");
  // Check if the two-factor-authentication is required
  if (myCloud.twoFactorAuthenticationIsRequired) {
    // Get the security code using node-prompt
    prompt.get(["Security Code"], async function(err, input) {
      if (err) return console.error(err);
      const code = input["Security Code"];
      // Set the security code to the instance
      myCloud.securityCode = code;
    });
  }
  else {
    // Now the 'ready' event fired but 'twoFactorAuthenticationIsRequired' is false, which means we don't need it (anymore)
    console.log("You are logged in completely!");
  }
});

Send Security Code Manually

When you are trying to login but two-factor-authentication is required, a security code will normally be sent to your devices automatically.

But sometimes, you need a new one, an SMS or a voice call. To request this programmatically, the API provides the method sendSecurityCode(mode) that sends a security code to your devices directly, via SMS or via Voice (phone call).

There exist 3 mode's. If you don't set a mode, the default one is message.

  1. message -> Sends a security code directly to your devices
  2. sms -> Sends an SMS containing a security code to your phone
  3. voice -> Calls your phone and speaks out a security code

Of course, the SMS will be sent to the phone number connected with your AppleID...

// Request SMS security code
myCloud.sendSecurityCode("sms");

// Request voice security code
myCloud.sendSecurityCode("voice");

Troubleshooting

Sometimes you run your script & give your credentials but no security code comes in? This mostly happens because you set up a session that is partial completed but not fully complete. Try to delete your existing session file and try it again.

Also try to not wonder why you do not have to type in a security code a second time after using test.js or any other test environment. They save your session at /tmp/icloud-session.json. If you use it a second time, it just uses the old session.

Applications

This is the basic iCloud API that you can use to interact with iCloud services.

Contacts

List

This method lists your account contacts data like groups and persons.

myCloud.Contacts.list(function(err, data) {
  if (err) return console.error(err);
  // Successfully your contacts :)
  console.log(data);
});

Demo: demo/contacts/listContacts.js

IMPORTANT

To use change(), new() & delete() you firstly have to list() the contacts one time to become a valid syncToken!

Change

To change a contact's properties, just change the object's properties and call change(myContact).

myContact.lastName = "NewLastName";

const changeset = await myCloud.Contacts.change(myContact);

Demo: demo/contacts/changeContact.js

Delete

Deleting a contact is also easy like changing. Just use a contact's object as argument when calling delete()

const delChangeset = await myCloud.Contacts.delete(myContactIDontLike);

Demo: demo/contacts/deleteContact.js

New

Creating a new contact is also really simple. You put a self-made contact-object as argument to new(). What a kind of properties a contact can have, is Apples work and this API just use apples JSON structures. If you need to know it, just loadan existing contact and have a look at it's object structure.

const createChangeset = await myCloud.Contacts.new({
  firstName: 'New',
  lastName: 'Guy',
  emailAddresses: [
    {
      label: "Privat",
      field: "[email protected]"
    }
  ],
  isCompany: false
});

Demo: demo/contacts/newContact.js

Friends

const locations = await myCloud.Friends.getLocations();

Please also remember that the fist requests maybe doesn't returns a position for the devices (Same thing when using FindMe) because the position is still in progress. Sometimes you have to wait a few seconds.

Demo: demo/friends/getLocations.js

iCloud Drive

The drive services are not completed yet.

This API is a little bit more complex because it implements a really useful implementation of unix paths in icloud. Please read the following paragraph to the end before using the API.

iCloud Drive's endpoints doesn't work with UNIX paths like /path/to/my/file. But iCloud Drive use something called drivewsid. That is an id that points on your items in iCloud Drive.

If you list a folder, it returns the drivewsid's of it's sub items. You can request them than.

The drivewsid of the root folder of iCloud Drive is FOLDER::com.apple.CloudDocs::root.

All methods of this API accept as first argument such a drivewsid.

Get Item

const itemInfo = await myCloud.Drive.getItem("FOLDER::com.apple.CloudDocs::root");

Demo: demo/drive/getItem.js

Error?

May you see the error "No PCS cookies". This is because you logged in too often and icloud servers does not send them to you anymore or your actual session just does not have any PCS cookies. In any case, wait a few seconds up to minutes and reset your session. And please store your sessions! This will avoid such problems!

If you do his, you will see that you become a list with root's sub items and the related drivewsid's. Theoretically you can work so.

Use UNIX file paths

But because I really like to use UNIX file paths directly I implemented a way that let you use them. That means you can just use a UNIX file path instead of a drivewsid if you know the path to your item.

const itemInfo = await myCloud.Drive.getItem("/myFolder/contains/my/nice/item.txt", undefined, true); // If using folder cache (Only necessary for UNIX paths) (Default is true)
Result

If you request a folder item, you get an array of the contained files within. But this array only contains general info about each file but not the whole contents. To get them, request the file seperetaly.

IMPORTANT

Because of the problem that my API hast to list every folder within the path of the requested item, this alernative is not very fast. For example if you request /Folder1/SubFolder/Item.txt the API hast to do 4 requests (root, Folder1, SubFolder & Item.txt).

But to speed it up, I implemented the possibility to cache everything. For example if you request /Folder1/SubFolder/Item.txt, every folder will be cached. Next time you request /Folder1/SubFolder/Item 2.txt, the folders on the way to Item 2.txt are not loaded again to speed it up. The last item in your path will ever be requested again each time. For example if you request /Folder1/SubFolder it needs 3 requests the first time (root, Folder1 & SubFolder). Now, if you request again /Folder1/SubFolder the cache will only be used for root and Folder1. SubFolder is the target item you want and therefore it will be request again.

To avoid using the cache generally you can set the third argument to false. Default it is true.

This is just a feature, I have implemented and if you do not like it, just use the docwsid's to request your data. That is much more native because iClouds internal API work in this way.

Result

How the strucuture of your requested item looks like, is the structure Apple created for their API's. In some cases I simplified some structures but all in all it's not my stuff.

To become an impression of such a result, just try it out :)

Rename Items

The renaming works really similar. It also accepts UNIX paths but also docwsid's. The only difference is that it accepts a list with items.

const changeset = await myCloud.Drive.renameItems({
  "/my/item/with/path.txt": "new name.txt", // Of course you can use a drivewsid
  "Oh/another/item.txt": "renamed.txt" // Of course you can use a drivewsid
}, undefined, true); // If using folder cache (Only necessary for UNIX paths) (Default is true)

But please remember that it can cost a lot of time when there many not cached folders on the way and you use UNIX paths.

Demo: demo/drive/renameItems.js

Delete Items

The same logic works when using deleteItems(). The only difference is that you don't use an object as argument but an array. Theoretically you can also use single string if you just have one item to delete.

const delChangeset = await myCloud.Drive.deleteItems([
  "/Test/this folder is new",
  "/Test/this folder is also new"
], undefined, true); // If using folder cache (Only necessary for UNIX paths) (Default is true)

But please remember that it can cost a lot of time when there many not cached folders on the way and you use UNIX paths.

Demo: demo/drive/deleteItems.js

Create Folders

This method provides functionality to create multiple folders in one parent folder

// 1st argument: '/Test' is the parent folder ( Can always be a 'drivewsid'! )
// 2nd argument: Array with new folders
const createChangeset = await myCloud.Drive.createFolders("/Test", ["this folder is new"], undefined, true); // If using folder cache (Only necessary for UNIX paths) (Default is true)

Demo: demo/drive/createFolders.js

Upload File

Still in progress

Calendar

The iCloud Calendar services have a little bit more complicated structure. Therefore I restructured the original objects a little bit to make it easier to work with the event data. That means it is theoretically possible that Apple makes significant changes on their API that stop my methods working.

Get Collections

Collection means just the different groups of events such like 'Private' or 'Work'. List your collections is a save way to know on which points you can define new events. You do it like the following:

const collections = await myCloud.Calendar.getCollections();

The structure of a collection is not so complicated, therefore I don't explain it detailed. Just have a look at it and try it out :)

Demo: demo/calendar/getCollections.js

Get Events

Please note that this maybe takes time because every event is requested for details one times. That's because the original Apple API returns the events not detailed if I request for an time area. I have to do another request for every event to get its detailed information. To get the progress of the complete method, just listen to progress event of your iCloud instance and have a look at parentAction: 'getEvents' and the related progress.

// Get all events from the 7th July 2017 to 9th August 2017
const events = await myCloud.Calendar.getEvents("2017-07-15", "2017-08-09");

Demo: demo/calendar/getEvents.js

Please keep always in mind that the possibility of working with Javascript's Date objects within events when getting them or creating is my personal workaround to make handling the API more easy. Apple's internal way of handling dates is an array driven solution that is not native as a Date-Object literal. Because of that it can be theoretically possible that you get a property I forgot to parse into Javascript's Date-Object. THis property would look like an array...

An event contains a lot of properties such like recurrence, alarms which explains the rules for repeating the event in future or every alarms that will be triggered. Just have a look at these objects to understand it detailed. It's not so complicated ;-)

Create Collection

const createChangeset = await myCloud.Calendar.createCollection({
  // Properties for your collection (Everything like id's will be calculated automatically)
  title: "Mein Kalendar 2!", // The name of the collection
  color: "#ffe070" // The color that will be displayed in iCloud clients and represent the collection (Optional default is #ff2d55)
});

A created collection gets a specific guid from Apple it will be identified with.

Demo: demo/calendar/createCollection.js

Create Event

Remember the same as you noticed for creating a contact. For creating a event, use the same object structure as you know from 'getEvents()'.

In this code example is not every possible property used. Everything that can be in a event you got, can also be here.

const createChangeset = await myCloud.Calendar.createEvent({
  title: "Viele", // Required
  location: "My City", // Optional
  description: "This is the best description ever", // Optional
  url: "https://maurice-conrad.eu", // Optional
  pGuid: "home", // 'guid' of collection
  alarms: [ // Lists all alarms (Optional)
    {
      before: true,
      weeks: 0,
      days: 0,
      hours: 0,
      minutes: 35,
      seconds: 0
    }
  ],
  recurrence: { // Describes the rule to repeat the event
    count: 3, // How many times the event will be repeated (Optional, default is Infinity)
    freq: "daily", // Type of frequence (e.g. 'daily', 'weekly')
    interval: 10 // Interval for frequence
  },
  startDate: new Date("2017-07-26 12:00"), // UTC Time is required from local, therefore the event start time means your local 12:00)
  endDate: new Date("2017-07-26 13:00") // Same here
});

Please keep always in mind that the possibility of working with Javascript's Date objects within events when getting them or creating is my personal workaround to make handling the API more easy. Apple's internal way of handling dates is an array driven solution that is not native as a Date-Object literal. Because of that it can be theoretically possible that you get a property I forgot to parse into Javascript's Date-Object. This property would look like an array...

Demo: demo/calendar/createEvent.js

Properties

Because the sepific properties to describe a task and its alarms, is part of Apple's internal API, I do not offer a documentation about this properties and their values because they can change every time.

To get an idea about the specific properties, try out some tasks and have a look at their objects literal using getOpenTasks() or getCompletedTasks(). The API's are transmitted directly without changes except datetime data. I try to parse it all into Date objects, so you don't have to care about the internal way of transmitting datetimes.

An exception I made is Recurrence you see below. But always keep in mind that the sepific properties used here may not work later... But normally Apple will not change their API every 5 minutes ;-)

Recurrence

The recurrence object contains a few properties that control the exact type of recurring the event. These properties are

{
  freq: "daily", // 'daily' || 'weekly', || 'monthly', 'yearly'
  count: null, // Count of recurring events, Default: Infinity for a never ending recurrence
  interval: 1, // Interval of frequency  
  byDay: null,
  byMonth: null,
  until: [Date], // Exact date on which the recurrence should be stopped
  frequencyDays: null,
  weekDays: null
}

The properties count & until contradict each other. Just use one of them to describe the recurrence.

Delete Event

// 'myEventIDontLike' is just an event object you got from 'getEvents()'

// 2nd argument: true - If you want to delete all recurred events ('recurrence') (Default is false)
const delChangeset = await myCloud.Calendar.deleteEvent(myEventIDontLike, true);

If you want to delete all recurring events after one special event, just set the 2nd argument to true. This will not delete all events before the event you set as 1st argument but all of the same type after it.

For example, if you have an event that starts at 7th of July and will be recurred every day and you want to delete all recurring events after the 10th of July, you have to use the event object of 10th of July as 1st argument and set the 2nd argument to true. This will delete all events of this type since the 10th of July but not the events before the 10th of July.

Demo: demo/calendar/deleteEvent.js

Change Event

// 'myEvent' is just an event object you got from 'getEvents()'
myEvent.location = "This place is much better";
// 2nd argument: true - If you want to change all recurred events ('recurrence') (Default is false)
const changeset = await myCloud.Calendar.changeEvent(myEvent, true);

Demo: demo/calendar/changeEvent.js

Photos

Get Photos

Please make sure that this service needs PCS-Cookies. Sometimes they aren't there because of some security aspects of iCloud (e.g. You take too many logins in a small time area). Don't care about this normally, just keep it in mind when something doesn't work as expected.

const images = await myCloud.Photos.get();

Demo: demo/photos/getPhotos.js

Upload Photo

Still in progress

Find Me

This service represents the Find my iPhone functionality of iCloud.

Attention

This service is very good protected by Apple. As you know from iCloud.com you have to retype your credentials to use FindMe when your session is older than a few minutes. That means, FindMe needs fresh cookies and therefore you have to retype your credentials. It is also so, that theoretically FindMe needs an initialization and handles the requests a little bit complicated. Therefore I implemented a solution that does everything automatically, you just have to give your credentials to the method and than everything works automatically. If the request is the first one in your session, my method will initialize everything and so on. Of course, this takes more time. After this initialization the method shall work faster.

Get

const devices = await myCloud.FindMe.get("[email protected]", "totally-safe-password");

You do not have to give your credentials again, these arguments are optional. Normally they will be used from the session instantly. (Your session remembers your password if you do not delete it).

Demo: demo/findme/findMe.js

Please also remember that the fist requests maybe does not return a position for the devices (Same thing when using Friends) because the position is still in progress. Sometimes you have to wait a few seconds.

That means in detail, that get() performs a new general initialization whose logic I want to prevent from you when using the API and, if needed, a new login when it is performed the first time. After that, the data will be requested normally. If your current instance always initialized FindMe in this way, the re-login will perfom only at the first time. After that your data is tried to get directly when using get(). But sometimes (after using it a few minutes) the cookies are too old and the data can not be requested. In this case an error occurs and you should set initialized property of your instance's FindMe object to false:

// 'get()' occured an error that the cookies are non-existing or too old
myCloud.FindMe.initialized = false;


// A new call 'get()' will re-initialize everything
const devices = await myCloud.FindMe.get("[email protected]", "totally-safe-password");

Sadly there are still problems with the API. Sometimes the cookies of the session are invalid in general and get() still does not not work.

In this case you have to reset your whole session :(

Play Sound

To play sound on a device, use the playSound() method.

// By giving a device's object
await myCloud.FindMe.playSound(myDevice);

// By giving just a device's ID
await myCloud.FindMe.playSound("XXX");

To get a device's object, just call FindMe.get(). This lists all devices and their related ID.

Demo: demo/findme/playSound.js

Reminders

This service represents the Reminders app of iCloud. Pleaae note that it's also not completed yet!

As I already said, I implemented Apples internal API's here and changed the object structure just when it was useful or to simplify the data. But the way what data is in which context given, is not my idea but Apples's. In this case the collections and the open tasks are represented by one single enpoint. I restructured the result a little bit but all in all, it is one method. The completed tasks are represented by an own endpoint that doesn't returns collections. And that is the way it works. To split collections and open tasks into two methods would just slow down performance because of the fact that you would need to call two requests that get exactly the same data. Of course, that would be the more sinful way because of the fact that the enpoint that for completed doesn't returns collections. But I can't do any thing.

Get Open Tasks and Collections

As I already explained, these methods return collections and within them, the tasks they are containing.

const tasks = await myCloud.Reminders.getOpenTasks();

Demo: demo/reminders/getOpenTasks.js

Get Completed Tasks

To get a list with all completed tasks

myCloud.Reminders.getCompletedTasks(function(err, tasks) {
  // If an error occurs
  if (err) return console.error(err);
  // All completed tasks (Not sorted by collections!)
  console.log(tasks);
});

Demo: demo/reminders/getCompletedTasks.js

Delete Task

// 'myTaskIDontLike' is just a task object we want to delete

const delChangeset = await myCloud.Reminders.deleteTask(myTaskIDontLike);

Demo: demo/reminders/deleteTask.js

Change Task

// 'myTask' is just a task object we want to change

myTask.title = "This is the new title :)"; // Don't use special characters like 'ä', 'ö', etc.
const changeset = await myCloud.Reminders.changeTask(myTask);

Demo: demo/reminders/changeTask.js

Create Task

const createChangeset = await myCloud.Reminders.createTask({
  title: "I have to do this!",
  pGuid: "tasks", // The 'guid' of a collection
  priority: 1, // 1 is "High", 5 is "Medium" & 9 is "Low",
  description: "This describes by task perfectly!"
});

Demo: demo/reminders/createTask.js

Properties

Because the sepeific properties to describe a task and its alarms, is part of Apple's internal API, I do not offer a documentation about this properties and their values because they can change every time.

To get an idea about the specific properties, try out some tasks and have a look at their objects literal using getOpenTasks() or getCompletedTasks(). The API's are transmitted directly without changes except datetime data. I try to parse it all into Date objects, so you don't have to care about the internal way of transmitting datetimes.

Complete Task

To complete a task, just do the following: (Please note that something like a uncomplete action is currently not implemented and therefore only possible manually. It's still in progress)

// 'myTaskICompleted' is the task you want to define as 'completed'

const chageset = await myCloud.Reminders.completeTask(myTaskICompleted);

Demo: demo/reminders/completeTask.js

Create Collection

const createChangeset = await myCloud.Reminders.createCollection({
  title: "My new collection",
  symbolicColor: "green" // Default 'auto'
});

Demo: demo/reminders/createCollection.js

A created collection gets a specific guid from Apple it will be identified with.

As you may know from Calendar, you were allowed to use hex color codes as #fa7b3d when using a color for a collection. Sadly, Reminders API does not allow this kind of setting colors. We are only allowed to use specific color codes as green, blue, red, orange, purple, yellow or brown

Change Collection

// 'myCollection' is just a collection we want to change
myCollection.symbolicColor = "green"; // Sets the symbolic color of the collection to 'green'
const changeset = await myCloud.Reminders.changeCollection(myCollection);

Demo: demo/reminders/changeCollection.js

Delete Collection

// 'myCollectionIDontLike' is just a collection object we want to delete

const delChangeset = await myCloud.Reminders.deleteCollection(myCollectionIDontLike);

Demo: demo/reminders/deleteCollection.js

Mail

Please make sure that you have Mail activated for iCloud.

Please keep in mind that the Mail service of iCloud and all related endpoints need some special cookies that are given when the first request for any of Mail's services is did. After that your session has the required cookies and the API's can be used. All of my Mail API's are designed in way that they repeat itself automatically if the cookies are needed and save them. Of course a sessionUpdate fires which makes it easy to save the session. Therefore you have to do nothing. I just say this because you shouldn't wonder if the first Mail API action takes longer than expected because your client has to do at least two requests instead of one.

Get Folders

Mailing systems in general work with folders that sort your mails. Such as Sent, Inbox, Deleted, VIP and your own folders.

const folders = await myCloud.Mail.getFolders();

Demo: demo/mail/getFolders.js

Just try it out and have a look at the structure :)

List Messages

// 'myFolder' is just a folder object we got from 'getFolders()'
// The 2nd and 3rd argument describe that we list the next 50 mails beginning at index 1 (first/newest mail)
// The sort order is descending / newest mails first
const messagesData = await myCloud.Mail.listMessages(myFolder, 1, 50);

Demo: demo/mail/listMessages.js

The message objects you get here just contains meta data about the message and a preview in plain text.

Get Message's Detail

Maybe you already noticed that a message object from 'listMessages' doesn't contains the complete mail. That's because a message can be very large. To get the complete message content you have to request it separately:

// 'myMail' is just a message object we got from 'listMessages()'
const messageDetail = await myCloud.Mail.getMessage(myMail);

Demo: demo/mail/getMessage.js

Move Messages

Please notice that you can only move messages that are part the same folder to a common destination folder with one API call. Otherwise an error will occur. To move multiple messages that are part of different folders to a common destination, you need multiple API calls.

// Items of the array are the messages you want to move (message objects)
// Has to be an array if you have more than one messages you want to move. If it's only one message, it's message object can be used as argument instead
const changeset = await myCloud.Mail.move([
  myMessage1, // The two messages have to be in the same folder!
  myMessage2 // Otherwise an error will occur
], destinationFolder); // 2nd argument is the destination folder (folder object)

Demo: demo/mail/moveMessages.js

Delete Message

// Items of the array are the messages you want to move (message objects)
// Has to be an array if you have more than one messages you want to move. If it's only one message, it's message object can be used as argument instead
const delChangeset = await myCloud.Mail.delete([
  myMessage1,
  myMessage2
]);

Demo: demo/mail/deleteMessages.js

Please keep in mind that a message you delete will normally just be moved to TRASH folder. Just if you delete a message within the TRASH folder, the message will be deleted completely.

Flag

Flagging is like marking/selecting your message. There are two existing flags iCloud Mail accepts.

  1. unread Mostly known as the blue point on the left side of your mail
  2. flagged A mostly orange flag or point on your message
Add Flag
// 1st argument is the message (myMessages is just an array containing message objects literal)
// 2nd argument is the flag you want to add. Possible values are 'flagged' and 'unread'. Default is 'flagged'.
const flagChangeset = await myCloud.Mail.flag([
  myMessages[0],
  myMessages[1]
], "flagged");

Demo: demo/mail/flagMessages.js

Remove Flag
// 1st argument is the message (myMessages is just an array containing message objects literal)
// 2nd argument is the flag you want to remove. Possible values are 'flagged' and 'unread'. Default is 'flagged'.
const unflagChangeset = await myCloud.Mail.unflag([
  myMessages[0],
  myMessages[1]
], "unread");

Please keep in mind that unread means really unread. That means to flag your message as read you have to unflag the unread flag.

Demo: demo/mail/unflagMessages.js

Send Mail

To send a mail, you should remember that if you do not use a from property, this method will try to get your iClouds mail address using a second API. If you didn't performed a __preference() method before (which will mostly be the case), the send() method will do this to get your iCloud's mail address. Of course only if you did not give it as property (from). And of course, such a second API call in background will take more time.

const sentment = await myCloud.Mail.send({
  //from: "Your Name<[email protected]>", If not given, your address will be found automatically
  to: "[email protected]", // Required
  subject: "Your API",
  body: "<strong>Hey Maurice,</strong><br><br>I totally love your <i>API</i>. Of course it's not perfect but <strong>I love it</strong>", // HTML string
  attachments: [ // Optional
    // Your attachments
    // Not implemented yet
    // Coming soon
  ]
});

Demo: demo/mail/sendMail.js

Create Folder

const createChangeset = await myCloud.Mail.createFolder({
  name: "My new Folder", // Default null
  parent: null // Can be a folder object or a guid string (Default null)
});

Sadly there exist a bug within the web application of iCloud. If you create a folder with this API, sometimes, the web app will need a lot of time to get the new folder. But if you delete or rename an existing folder, your new folder becomes visible. I don't know the reason but all in all, your folder exist in iCloud's database and a getFolders() call will return the new folder.

Demo: demo/mail/createFolder.js

Move Folder

Please keep in mind that you are not allowed to move one of iCloud's default folders such as "Inbox", "Drafts", "Archive", "Deleted Messages" or "Junk". You are only allowed to move your own folders with the role FOLDER;-)

// 'myFolder' is just a folder object
// 'myTargetFolder' is also just a folder object
const moveChangeset = await myCloud.Mail.moveFolder(myFolder, myTargetFolder);

Demo: demo/mail/moveFolder.js

If you want to move a sub folder from its parent folder to "global" level without a parent, set the 2nd argument (targetFolder) to. This will remove the parent property and your folder is global.

Rename Folder

Please keep in mind that you are not allowed to rename one of iCloud's default folders such as "Inbox", "Drafts", "Archive", "Deleted Messages" or "Junk". You are only allowed to rename your own folders with the role FOLDER;-)

// 'myFolder' is just a folder object
const renameChangeset = await myCloud.Mail.renameFolder(myFolder, "New Name");

Demo: demo/mail/renameFolder.js

Sadly there exist a bug within the web application of iCloud. If you rename a folder with this API, sometimes, the web app will need a lot of time to update the folder. But if you delete or rename an existing folder in the web app, your folder's name becomes visible. I don't know the reason but all in all, your folder exist in iCloud's database and a getFolders() will return the correct name.

Delete Folder

// 'myFolder' is just a folder object
const delChangeset = await myCloud.Mail.deleteFolder(myFolder);

Demo: demo/mail/deleteFolder.js

Sadly there exist a bug within the web application of iCloud. If you delete a folder with this API, sometimes, the web app will need a lot of time to get the updated folder list. But if you delete or rename an existing folder in the web app, your deleted folder would stop being visible. I don't know the reason but all in all, your folder exist in iCloud's database and a getFolders() will return the correct folder list.

Notes

Attention

The database of your notes can be very large. And therefore the internal API of Apple uses records. A normal iCloud client requests for a zone (There is only one zone called Notes) and gets just a few notes. Now, if the user scrolls down, more records will be requested. That is the way how Apple's API works.

To make this process more dynamic, use the fetch() method which gets all notes as records asynchrounsly.

Note: By default a Note record

Fetch Notes

When using fetch() all Note records will be stored at the __notes key within your notes emitter (return of get()). If you want to know the parent folder of a note given in __notes while the related folder is unknown at the moment, you can use getFolder() instead of waiting until the related folder comes trough get().

// Get all notes
const notes = myCloud.Notes.fetch();

notes.on("data", zone => {
  // Do not use zone by default, the notes are sorted into 'folders'

  // 'folders' is an array containing all notes sorted by folders that are known at this point of time
  // Because of the fact, that the folders are coming over a zone, it may takes some time
  console.log(notes.folders);

  // Just log the notes you already got
  //console.log(notes.__notes);
});

notes.on("end", () => {
  // Now, you can be sure that EACH folder and EACH note are known
  console.log(note.folders);
})

Demo: demo/notes/fetch.js

Resolve Notes

Because fetch() returns Note records just containing TitleEncrypted SnippetEncrypted fields, call resolve() to get the Note's full content and encode the base64 snippet and title automatically. As arguments pass Note records or their recordName instead.

// Returns array of your notes detailed
const notesDetailed = await myCloud.Notes.resolve(note1, note2); // note1 and note2 are Note record objects or 'recordName' strings

console.log(notesDetailed);

Demo: demo/notes/resolveNotes.js

Get Folders

Get folder objects directly. Use this method if you need a Note's parent folder until this folder is unknown at the moment.

// 'myFolder1' & 'myFolder2' can be folder objects but also a plain 'recordName' as string (Useful if you know a folder's 'recordName' from a 'Note' record but not it's whole object because it is a record you actually don't have)
myCloud.Notes.getFolders([ // Can also be a single folder object or a single 'recordName' string
  myFolder1,
  myFolder2
], function(err, folders) {
  // If an error occurs
  if (err) return console.error(err);
  // Array with your folder's data
  console.log(folders);
});

Demo: demo/notes/getFolders.js

Push Services

To use push services to recognize when something changes, you need to register a service for being notified by Apple's push service.

You just have to follow these instructions:

Initialization

Because a push notification is more less an infinity loop of requests, the programmatically logic behind listening for push services keeps your script running.

Because this is may not your intention, the logic behind push services will not be initialized automatically when initializing your instance. You have to do it manually:

Sometimes, your token is expired. In this case, the method initPush() will automatically request a new one. But of course, this takes a little bit more time.

// This initializes your instance to listen for push notifications in general
myCloud.initPush();

// Your script will keep running until you do not shut it down manually

Push Event

Getting push notifications:

myCloud.on("push", function(notification) {
  console.log("New push notification!");
  console.log(notification);
});

Please note, that you can omly get push events for registered services.

Events

There are a few events you can listen to. For example the progress event that fires every time, an action is divided into some sub actions that need time. For example the login process fires three progress events.

If you getEvents() from Calendar, the count of fired progress events is related to the total amount of events that have to be requested for detailed information about them.

As I already explained, the FindMe service often requires a new login. The get method of FindMe does this automatically. And in this case a new login() will be executed which means that there will also three progress events fired.

Progress Event

myCloud.on("progress", function(event) {
  console.log(event);
});

Result

{
  parentAction: "yourMethod", // The method name of the method the action is related to
  action: "what-currently-happens", // A special keyword that describes the current action
  progress: 0.5 // Number between 0 and 1
  /*

    Some specific properties of the current action

  */
}

Ready Event

This event is used when you call the iCloud instance with a session and your credentials. (Recommended way of initialization). Then, the ready fires when your session is valid or (if not) the automatic login process will be finished. It is really useful if you want to write less code and a clean way initialization including all kinds of error handling such as invalid credentials, invalid sessions or expired cookies.

myCloud.on("ready", function() {
  // Your iCloud instance is ready to use!
});

Please note that the time your instance needs to be ready is related to the fact wether you already have a valid working session or your session has to be re-initialized completely (because cookies are expired or something else is broken). In such a case a complete new login() process will be executed which takes time.

Session Update Event

This event fires every time when the current session updates. For example, if new cookies are request (e.g. when using FindMe). It fires also after a normal login. You should use it to save your session at this point.

myCloud.on("sessionUpdate", function() {
  // Session changed
});

Error Event

Please note, the err event is not used to determine wether a method does not work. In such a case, you get an error thrown normally by a rejected promise or, if you are using the callback function, as 1st argument. The err event is fired every time an error occurs but the method is not failed instantly. E.g. If you do not have a session, your cookies are too old or a broken one, an err event fires that determines that your instance was not abled to use your current session. But of course, the instance will login your account a second time and is trying to create a new session. Or, if you log in but a two-factor-authentication is required, an err event fires. In this case you should enter your code as explained in Two Factor Authentication.

Error Event

myCloud.on("err", function(event) {
  console.error(event);
});

Important

I do not support any malicious use or abuse of this project that harms third parties, steals data or violates personals rights. This project is made for personal use only with your personal account and not to make use of third parties private data such as tracking or manipulating accounts. You are not allowed to abuse functions or algorithms of this project for criminal activities.

Improvements

Any questions or improvements? Open a new issue on GitHub or mail me at [email protected] :)

Not finished

µ Of course this module is not finished. Not all services are completed, there is always stuff to do. At the moment, it is still in progress and I'm working on it much as possible :)

Thanks

Thanks for your Attention. It took a lot of time to implement this prject in the current state. Just to understand Apple's API's was really hard work. I lost more than 100 hours in this project. Hope, you enjoy :)

icloud-api's People

Contributors

benmccann avatar calvintwr avatar davidengelmark87 avatar dependabot[bot] avatar fmalekpour avatar kopiro avatar mauriceconrad avatar quochuy3191313 avatar velezjose avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

icloud-api's Issues

It seems that `self.account.webservices` is undefined when `getHostFromWebservice` is called

I tried to run the demos and got this for getOpenTasks and similar to this for getCollections:

UnhandledPromiseRejectionWarning: TypeError: Cannot read property 'reminders' of undefined
    at iCloud.getOpenTasks (......./node_modules/apple-icloud/resources/apps/Reminders.js:7:63)
    at Object.main.<computed> [as getOpenTasks] (......./node_modules/apple-icloud/resources/helper.js:84:27)

I am not sure if it's because Apple's API is updated or not. If you can give me directions, I could make a pr too to contribute.

TypeError: myCloud.Notes.getAll is not a function

Hi, thanks very much for taking the time to create this brilliant API.

I'm just wondering if the latest version of the Notes helper is committed?

I ask because the example readme talks about using the functions getFolders(), and the getFolders.js example uses uses a getAll() function. Both of which don't exist in /resources/apps/Notes.js.

As a result, the examples relating to Folders don't work and throw a type error.

They do however exist in the Mail.js resource

[Feature Request] iCloud Mailing Rules API

Is there any way by which I can create/delete/reassign mail filters? Right now I can do this in iCloud Web Mail but clicking on the Settings cog (bottom left) and Rules. This allows me to filter server-side i.e. as an email arrives it automatically pushes the mail to the respective folder.

Example code failing with TypeError

A TypeError is thrown from running your example login code

You are logged in successfully!
(node:10120) UnhandledPromiseRejectionWarning: TypeError: Cannot read property 'ckdatabasews' of undefined
    at iCloud.__zone /Documents/noteconverter/node_modules/apple-icloud/resources/apps/Notes.js:178:63)
    at Object.main.<computed> [as __zone] (/Documents/noteconverter/node_modules/apple-icloud/resources/helper.js:84:27)
    at iCloud.fetch (/Documents/noteconverter/node_modules/apple-icloud/resources/apps/Notes.js:62:16)
    at Object.main.<computed> [as fetch] (/Documents/noteconverter/node_modules/apple-icloud/resources/helper.js:84:27)
    at iCloud.<anonymous> (/Documents/noteconverter/prompt-credentials.js:37:37)
    at processTicksAndRejections (internal/process/task_queues.js:94:5)
(node:10120) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 1)
(node:10120) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

This is the code that is being run (along with prompt-credentials.js):

const promptiCloud = require("./prompt-credentials");

(async () => {
  // Login to icloud and ask for new credentials if needed
  const myCloud = await promptiCloud();

  // Get all notes
  const notes = myCloud.Notes.fetch();

  var done = false;

  notes.on("data", (zone) => {
    console.log(
      "Fetched: " +
        notes.__folders.length +
        " folders; " +
        notes.__notes.length +
        " notes"
    );

    // Log folders structured
    console.log(notes.folders);
  });
})();

Find My Friends (FMF) Webservice no longer exists

It looks like Apple quietly removed using Find My Friends from iCloud in the browser sometime September 2021. This means that the fmf webservice is not returned by Apple.

Just letting anyone that is looking to use FMF through this project that it will not work.
If anyone is wondering if other services like FindMe still work, as of 4/27/2023 I was able to get it to list my devices.

Available webservice endpoints as of 4/27/2023 (No FMF):
Code_cFZ5s3D4L0

Only 500 contacts

I can only get first 500 contacts ..

Here is my code

`

// Require the module
const iCloud = require('apple-icloud');

var session = {}; // An empty session. Has to be a session object or file path that points on a JSON file containing your session
var username = "xxx"; // Your apple id
var password = "xxxx"; // Your password

const readline = require('readline').createInterface({
input: process.stdin,
output: process.stdout
})

var myCloud = new iCloud("icloud-session.json", username, password);

myCloud.on("ready", function() {
console.log("Ready Event!");
// Check if the two-factor-authentication is required
if (myCloud.twoFactorAuthenticationIsRequired) {
// Get the security code using node-prompt
readline.question("Enter Code:", (code) => {
myCloud.securityCode = code;
readline.close()
});
}
else {
console.log("You are logged in completely!");
myCloud.Contacts.list(function(err, data) {
if (err) return console.error(err);
// Successfully your contacts :)
// console.log(data);
console.log(data.contacts.length);
});
}
});

`

Support for/Examples of long-running instances?

Not an issue as much as a question. Do you or anyone else have experience with a long-running instance of this? What I means is should I tear down my iCloud instance each time or reuse it? I am sharing session but I have a nodejs process that I am writing to be able to query it's data via voice (Alexa skill) or other UI/CLI tools.

I am planning on polling the FmF endpoints every 1-2 minutes so I can write some code to determine location of my partner and I for geo-related things ("X in on the way home", "Y remember to do Z", Turn off all lights when we both leave, etc). I can keep calling the getLocations() method but I didn't know if there was any "Session validity"/"Session expiration" checking and/or if it would refresh/renew the session if it had expired.

My guess is that it won't work and I should just re-use the session but recreate the iCloud instance to force-rechecking. That plus some system to re-enter my 2FA code remotely (maybe web endpoint with simple form that get's texted to me when it's needed? How is everyone else handling this, even for cron-type jobs this would be an issue right?).

Cannot create event with latest git

I'm trying to follow the samples to create an event, but unfortunately, I'm getting this:
`b/node_modules/iCloud-API/resources/helper.js:5
if (!(key in a)) {
^

TypeError: Cannot use 'in' operator to search for 'pGuid' in undefined
at Object.keys.forEach.key (/opt/varianteb/node_modules/iCloud-API/resources/helper.js:5:13)
at Array.forEach ()
at fillDefaults (/opt/varianteb/node_modules/iCloud-API/resources/helper.js:4:17)
at iCloud.createEvent (/opt/varianteb/node_modules/iCloud-API/resources/apps/Calendar.js:169:24)
at Object.main.(anonymous function) [as createEvent] (/opt/varianteb/node_modules/iCloud-API/resources/helper.js:84:27)
at iCloud. (/opt/varianteb/server/boot/routes.js:320:38)
at iCloud.emit (events.js:189:13)
at iCloud.EventEmitter.emit (domain.js:459:23)
at sessionInit (/opt/varianteb/node_modules/iCloud-API/main.js:130:14)
`
Can somebody point me to what I'm doing wrong here ?
Thanks,
Nick

Application Specific Password Support

While I can get this library to work with my personal AppleID I was hoping it would also support Application Specific Passwords. Unfortunately it doesn't seem to be the case in my testing.

From an authentication perspective it shouldn't be any different - probably easier because there is no need to use a security code with an Application Specific Password. Also more portable when you don't have to deal with a security code as well.

Are there any plans for getting it to work with Application Specific Passwords?

Upgraded Reminders don't seem to work

When I try and use your demo code to pull the reminders I'm greeted by the two reminders that Apple inserts after you upgrade the reminders backend. Is there any way to pull post upgraded to iOS 13 reminders?

Reminders returned:
Array(2) ["Where are my reminders?", "The creator of this list has upgraded these remind…"]

  • Edited to remove the "Test" reminder that I put in to make clearer

Getting 421 code in get_events() on catch.

Getting 421 code in get_events() on catch. is this 421 or 4,2,1 i found some error code in apple dev site like 1 for case noCalendar = 1, 2 for case noStartDate = 2, 4 for case datesInverted = 4. is there any option to find why this code showing?

Cannot create new event

I've tried to create new event to iCloud calendar by using the demo code in demo/calendar/createEvent.js. Unfortunately, after it showed me that You are logged in successfully! and then broken down. I noticed that iCloud server responded 400 error by output the raw response defined in resources/apps/Calendar.js line 416. The full response is attached below:

{
  "statusCode": 400,
  "body": "",
  "headers": {
    "server": "AppleHttpServer/e70a1a237a4f",
    "date": "Wed, 09 Oct 2019 14:07:51 GMT",
    "content-length": "0",
    "connection": "keep-alive",
    "x-apple-jingle-correlation-key": "TQ3MNZ5VEVG2HOZY",
    "apple-seq": "0",
    "apple-tk": "false",
    "apple-originating-system": "UnknownOriginatingSystem",
    "x-responding-instance": "caldavj:33400801:mr22p34ic-ztbu09031801:8501:1918B268:8d279581b458",
    "access-control-allow-origin": "https://www.icloud.com",
    "access-control-allow-credentials": "true",
    "set-cookie": [
      "X-APPLE-WEBAUTH-TOKEN=\"v=2:t=IAAAAAAABLwIAAAAAF2d6bcRDmdzLycpqWCFPKlOROgyScUewbED8sW2HEpgBT-5seqgKiWM1SdpbB-..."
    ],
    "x-apple-api-version": "v1",
    "via": "xrail:mr35p00ic-qugw03170401.me.com:8301:19B224:grp21, f8995afd047f3cef862ffdc1f64276f8:Hong Kong",
    "strict-transport-security": "max-age=31536000; includeSubDomains;",
    "x-apple-request-uuid": "9c36c6e7-b525-4dab-a234-23e4fceeeeee",
    "access-control-expose-headers": "X-Apple-Request-UUID, Via"
  },
  "request": {
    "uri": {
      "protocol": "https:",
      "slashes": true,
      "auth": null,
      "host": "p34-calendarws.icloud.com",
      "port": 443,
      "hostname": "p34-calendarws.icloud.com",
      "hash": null,
      "search": "?clientBuildNumber=17DProject...",
      "query": "clientBuildNumber=17DProject...",
      "pathname": "/ca/events/home/42A6A098-CB75-A5C4-55BA-.../",
      "path": "/ca/events/home/42A6A098-CB75-A5C4-55BA-.../",
      "href": "https://p34-calendarws.icloud.com/ca/events/home/42A6A098-CB75-A5C4-55BA-..."
    },
    "method": "POST",
    "headers": {
      "Host": "p34-calendarws.icloud.com",
      "Cookie": "X-APPLE-WEBAUTH-HSA-TRUST=\"...",
      "Content-Length": 1568,
      "Referer": "https://www.icloud.com/",
      "Content-Type": "text/plain",
      "Origin": "https://www.icloud.com",
      "Accept": "*/*",
      "Connection": "keep-alive",
      "Accept-Language": "en-us",
      "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/604.1.25 (KHTML, like Gecko) Version/11.0 Safari/604.1.25",
      "X-Requested-With": "XMLHttpRequest"
    }
  }
}

"Cannot read property 'map' of undefined" in Notes.js after unhandled server error

When a request to resolve a note fails on the server side (e.g. 400 Bad Request), a JSON will be returned that doesn't a "results" field.

This was the payload returned from the server from at least one of the notes in my iCloud account:

{
  "uuid" : "2487d368-fc70-410d-8c0b-ec957399b58a",
  "serverErrorCode" : "BAD_REQUEST",
  "reason" : "BadRequestException: missing required field 'value'"
}

Therefore, this line will produce a TypeError since result.results will be undefined:

const records = result.results.map(res => res.rootRecord).map(record => {

Exception stack trace:

TypeError: Cannot read property 'map' of undefined
    at Request._callback (y:\src\icloudapi-test\node_modules\apple-icloud\resources\apps\Notes.js:139:40)
    at Request.self.callback (y:\src\icloudapi-test\node_modules\request\request.js:185:22)
    at Request.emit (events.js:182:13)
    at Request.<anonymous> (y:\src\icloudapi-test\node_modules\request\request.js:1154:10)
    at Request.emit (events.js:182:13)
    at IncomingMessage.<anonymous> (y:\src\icloudapi-test\node_modules\request\request.js:1076:12)
    at Object.onceWrapper (events.js:275:13)
    at IncomingMessage.emit (events.js:187:15)
    at endReadableNT (_stream_readable.js:1091:14)
    at process._tickCallback (internal/process/next_tick.js:174:19)

The reason was that at least one of my notes fetched by myCloud.Notes.fetch() had no "shortGUID" field.
Filtering out this note fixed the error for me, but there should be a general handling of Bad Request errors.

Apart from that, thanks for the great work on this project!

{ reason: 'Missing X-APPLE-WEBAUTH-TOKEN cookie', error: 1 }

Trying to fetch contacts from iCloud. On contacts.list method I am getting this error

{ reason: 'Missing X-APPLE-WEBAUTH-TOKEN cookie', error: 1 }

Kindly suggest why I am getting this error.

On inspecting auth.cookies, this contains following element
'X-APPLE-WEBAUTH-LOGIN':
'X-APPLE-WEBAUTH-VALIDATE':
'X-APPLE-WEBAUTH-HSA-LOGIN':
'X-APPLE-WEBAUTH-USER':
'X_APPLE_WEB_KB-GKJULTVXCERN2NRGJRHWFXVRFLK':

Contacts `new` function returns 400 Bad Request

Code to reproduce:

// .. logged in

// list contacts
await myCloud.Contacts.list();

// attempt to create a new contact (w/ traditional promise syntax)
myCloud.Contacts.new({
  firstName: 'Maurice',
  lastName: 'Conrad',
  emailAddresses: [
    {
      label: 'Private',
      field: '[email protected]',
    },
  ],
}).then(console.log).catch(console.error);

The output of the above code is as follows:

{ 
  requestUUID: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx',
  errorReason: 'Bad Request',
  errorCode: 400 
}

I checked the request builder in resources/apps/Contacts.js and it looks correct based on what a quick iCloud network inspector check looked like.

Returns invalid url for photos

#Contacts.new - Invalid sync token

When using #Contacts.new to add a new contact, I will get the following error message:

"requestUUID":"4758bb9d-c842-4efc-8c66-b6e3dc2d18e4","errorReason":"Invalid sync token","errorCode":420

Per documentation, I have used #Contacts.list prior before invoking #Contacts.new like this:

cloud.Contacts.list().then(data => {
     return cloud.Contacts.new(validData)
})

Assume that validData has valid data.

Unable to create a new reminder

I have been unable to create a reminder, using the example code.

I get the following stack trace:

SyntaxError: Unexpected end of JSON input
    at JSON.parse (<anonymous>)
    at Request._callback (./node_modules/apple-icloud/resources/apps/Reminders.js:110:35)
    at Request.self.callback (./node_modules/request/request.js:185:22)
    at Request.emit (events.js:193:13)
    at Request.<anonymous> (./node_modules/request/request.js:1161:10)
    at Request.emit (events.js:193:13)
    at IncomingMessage.<anonymous> (./node_modules/request/request.js:1083:12)
    at Object.onceWrapper (events.js:281:20)
    at IncomingMessage.emit (events.js:198:15)
    at endReadableNT (_stream_readable.js:1139:12)

which is because body is an empty string here: https://github.com/MauriceConrad/iCloud-API/blob/master/resources/apps/Reminders.js#L110

I am trying to troubleshoot this (changing a task works, for instance) but any help would be appreciated, thanks!

Ability to get health app data from iCloud

I'm wondering if there is any way through this to get the apple health app data that is sync'd with iCloud. It shows up as using up space in my iCloud by I can't directly access the data any way that I know of, and I was wondering if there is some way you could build in a method to access it in this app.

Constant two-factor authentication notifications when playing sounds on iPhone

Hi there.
Every time I run this below:

require("dotenv").config();

let username = process.env.ICLOUD_EMAIL;
let password = process.env.ICLOUD_PWD;

module.exports = function () {
  return new Promise(async (reso, reje) => {
    let myCloud = await require("./prompt-credentials")();
    let devices;
    try {
      devices = await myCloud.FindMe.get(username, password);
    } catch (e) {
      return reje(e);
    }

    const myPhone = devices.content[0];
    try {
      const res = await myCloud.FindMe.playSound(myPhone.id);
      reso(res);
    } catch (e) {
      reje(e);
    }
  });
};

It plays the sound, but I always get a notification saying that a new sign-in is happening from my server.
Is there any way to remedy this?

Photos only return 100 results

Hey there, this package is awesome, but I just have a question about Photos functionality.

I can successfully fetch photos but I only get 100 results. I've tried to dabble with the resultsLimit and interestingly when I set it to 50, I get back 25 results. With other numbers it always returns half of them, but it is capped at 100 results.

Is there a paging that needs to be implemented, or do you have any idea what could cause this? Thanks!

version from npm package can't login with credentials

Hi,

when using npm install apple-icloud version 1.1.0 is installed which has bug in self.isLogged property - it always evaluates to true (because of username ? (self.username === username) not put in braces), preventing login with credentials.

      self.loggedIn = (function() {
        //console.log(self.auth.cookies.length > 0, !!self.auth.token, self.account != {}, self.username === username);
        return (self.auth.cookies.length > 0 && self.auth.token && self.account != {} && username ? (self.username === username) : true);
      })();

I see it's already fixed in this repository but not in release.

BTW I've also tried using apple app-specific passwords (https://support.apple.com/en-us/HT204397) but with no luck. Do you happen to know if it's even possible?

Playsound demo errors and crashes with fillDefaults not a function

Seems like some code was missed when fillDefaults was changed to be a helper

I think the problem code is lines 206-210 inf resources/apps/FindMe.js.

I will fork and create a pull request.

This was the error

TypeError: {(intermediate value)(intermediate value)}.fillDefaults is not a function
at /home/david/appleicloud/iCloud-API/resources/apps/FindMe.js:210:11
at new Promise ()
at iCloud.playSound (/home/david/appleicloud/iCloud-API/resources/apps/FindMe.js:199:12)
at Object.main. [as playSound] (/home/david/appleicloud/iCloud-API/resources/helper.js:84:27)
at /home/david/appleicloud/iCloud-API/demo/findme/playSound.js:43:38
at processTicksAndRejections (internal/process/task_queues.js:85:5)

TypeError: Cannot read property 'zoneID' of undefined

this error comes from const images = await myCloud.Photos.get();

TypeError: Cannot read property 'zoneID' of undefined
at Request._callback (C:\Users\Home\Desktop\apple\node_modules\apple-icloud\resources\apps\Photos.js:69:41)

Unnecessary 2FA Prompt and notification email

Just wondering if this is happening to anyone else. I've gotten everything working, and my scripts execute correctly without any errors.

However, every time it runs, I get a prompt on my Apple Devices (iPhone, Mac) to do 2FA, and an email indicating that a new devices has signed into iCloud. Obviously this was necessary the first time to input the code, but these subsequent requests seems to have no effect on the script running. I.e. it runs correctly regardless of what I do with the prompt, (ignore, dismiss, etc)

I believe that I am using my session JSON file correctly, otherwise I doubt I would be able to access items like Reminders and Find my Friends without having to reauthenticate. (my scripts do not have my AppleID or passwords in them, these are only in the JSON)

Any insight on what might be causing these 2FA prompts and notification emails, or is this just something with Apple that is unavoidable?

You need to publish latest fix to NPM! :)

Hey, the module on NPM is currently broken, and it looks like that the fix is your latest commit:

➜  track-apple-location diff node_modules/apple-icloud/main.js ~/Projects/iCloud-API/main.js
114,115c114,115
<         console.log(self.auth.cookies.length > 0, !!self.auth.token, self.account != {}, self.username === username);
<         return (self.auth.cookies.length > 0 && self.auth.token && self.account != {} && username ? (self.username === username) : true);
---
>         //console.log(self.auth.cookies.length > 0, !!self.auth.token, self.account != {}, self.username === username);
>         return ((self.auth.cookies.length > 0) && (self.auth.token && self.account != {}) && (username ? (self.username === username) : true));
135a136

cd3d968

"TypeError: Cannot read property 'emit' of undefined" when unable to write session file

When attempting to run the demo applications on Windows 10 under Cygwin where the directory to which the session file would have been written did not exist, the attempt to emit this fact failed due to "TypeError: Cannot read property 'emit' of undefined."

The problem can be solved by setting var self = this at the top of the saveSession() method in main.js and referencing that variable rather than directly referencing this.

Note with missing shortGUID is not handled

When a note that has no shortGUID field is passed to Notes.resolve(...), the iCloud will respond with a HTTP 400, the response payload of which is not handled in Notes.resolve(), leading to a follow-up error (see #51 )

Invalid data in a note should be handled before submitting the resolve request to the server.

FindMe

Hi,

Thanks for this module. However I have some issues with it.

I have the following code:

I will create an iCloud instance with session, username, password. This I store in a global var and

I use this to load every 5 minutes new findme data from the apple server using

GlobalVar.FindMe.get(username,password)

This works for 20 minutes or so, Sometimes more sometimes less and then got this error:

{ error: 'Something went wrong. Maybe your credentials are incorrect.',
code: 11 }

Then I call GlobalVar.FindMe.initialized = false; and continue. with FindMe.Get. This won't work I get the same errors.

Then I create again a new instance and store it to the globalvar and continue. Same errors. If I kill the app and restart it works.

What I am doing wrong here. Does the iCloud instance takes care of reconnecting or does the FindMe.Get this (as I also pass username and password to this as well).

Could you please help me out and describe the steps for getting the findme info on an x interval? and what I need to do when there is an error?

Thanks!

Reminders broken for iOS 13 / Catalina

Apple introduced a new reminders app in iOS 13 and macOS Catalina. The node API doesn't seem to play nice with this new reminders data, instead returning a message saying you need to update to iOS 13 and/or Catalina. Would be great to see this issue worked on/resolved.

TypeError: Cannot read property 'length' of undefined

I'm getting an error with the getEvents method.

It's on the following var requestEventsCount = result.Event.length;

TypeError: Cannot read property 'length' of undefined

It seems that is there is no event, the Event object is undefined.

notes api

Q pls from where you got those endpoints?

Bruno

Failed to parse - 502 Bad Gateway

Seems like Apple will ban requests for iCloud contacts, causing a 502 error?

I get a JSON parse failure when making requests via #Contacts.list. When I print the response received, I get

<html>\r\n<head><title>502 Bad Gateway</title></head>\r\n<body>\r\n<center><h1>502 Bad Gateway</h1></center>\r\n<hr><center>Apple</center>\r\n</body>\r\n</html>\r\n"

I am able to continue using the service with another server or when I run it on my own computer.

Incompatibility with Express ?

Hi,
I'm trying to integrate your lib with an existing application (Express (Loopback)).
The rest of the application crashes with random errors like 'cannot read property 'modelName' of undefined. Do you know what it could be ?
Currently I've only:

var iCloud = require("apple-icloud");

Thanks,
Nick

Auth with app specific password?

Hi, first of all thanks for the great lib!

I'm trying to add an iCalendar connection to my web app so I can read/create/update/delete events on my users' behalf.

However I can't ask them for their icloud password for security reason and for such cases apple has a feature called "application specific password" where a user can generate a password to grant access to a specific 3rd-party app (which looks like "tpke-ewiz-olyv-dfds").

When I try to use such password it gives me "Account is broken or password and username are invalid".

Maybe you could provide any input on what should be changed in the auth flow to make those credentials work? (like any specific auth url?) Can't quite find any info on that as of now so any help is much appreciated!

Thanks!

const trusting = await this.trust(self) FAIL?

Hi! I have fixed a couple things in the source code locally to get things working a bit better, but I am stuck here..

It prompts me for my 2 factor authentication, and I type in the 6 digit number. But then it says I'm logged in, but I'm not. I'm using your standard workflow.

In setup.js there's some code...

        const trusting = await this.trust(self);

        // Use /trust's headers (X-Apple-Twosv-Trust-Token and new authentication token)
        if ("x-apple-session-token" in trusting.response.headers) {
          self.auth.token = trusting.response.headers["x-apple-session-token"];
        }
        if ("x-apple-twosv-trust-token" in trusting.response.headers) {
          self.auth.xAppleTwosvTrustToken = trusting.response.headers["x-apple-twosv-trust-token"];
        }

Well, trusting.response.headers does not have those keys. Anyone else have these problems?

Crash: self.account is undefined

Hello, first of all thanks for fixing #19!

Unfortunately I am now unable to run any of the examples. It seems that self.account is always undefined.

I get the following crash scan:

(node:18246) UnhandledPromiseRejectionWarning: TypeError: Cannot read property 'reminders' of undefined
    at iCloud.getOpenTasks (/home/adrien/git/roam-export/node_modules/apple-icloud/resources/apps/Reminders.js:7:63)
    at Object.main.<computed> [as getOpenTasks] (/home/adrien/git/roam-export/node_modules/apple-icloud/resources/helper.js:84:27)

Here is code that triggers it, it's the example from the README:

const myCloud = new iCloud(
    "icloud-session.json",
    process.env.ICLOUD_LOGIN,
    process.env.ICLOUD_PASSWORD
);

myCloud.on("ready", async function () {
    if (myCloud.twoFactorAuthenticationIsRequired) {
        prompt.get(["Security Code"], async function (err, input) {
            if (err) return console.error(err);
            const code = input["Security Code"];
            myCloud.securityCode = code;
        });
    } else {
        console.log("You are logged in completely!");
        const collections = await myCloud.Reminders.getOpenTasks(
            (err, tasks) => {
                if (err) console.error(err);
                else console.log(tasks);
            }
        );
        console.log(collections);
    }
});

I get the same issue with the demo examples, for any of the services.

I am not using 2FA, and it looks like I am logged in successfully because myCloud.loggedIn === true.

Play Sound on iPhone

Thanks for your work on this API! I would like to use it to play a sound on my iPhone (within the FindMe feature). Is there a way to do this currently, or do you have plans to implement it?

Cannot read properties of undefined (reading 'findme')

Upon running
await myCloud.FindMe.get(username, password),
this error pops up

Full error:
TypeError: Cannot read properties of undefined (reading 'findme') at iCloud.__start (/Users/derrickw/VS Code Projects/Find My (Notifications)/node_modules/apple-icloud/resources/apps/FindMe.js:32:63) at Object.main.<computed> [as __start] (/Users/derrickw/VS Code Projects/Find My (Notifications)/node_modules/apple-icloud/resources/helper.js:84:27) at iCloud.__saveGet (/Users/derrickw/VS Code Projects/Find My (Notifications)/node_modules/apple-icloud/resources/apps/FindMe.js:88:19) at Object.main.<computed> [as __saveGet] (/Users/derrickw/VS Code Projects/Find My (Notifications)/node_modules/apple-icloud/resources/helper.js:84:27) at /Users/derrickw/VS Code Projects/Find My (Notifications)/node_modules/apple-icloud/resources/apps/FindMe.js:14:19 at new Promise (<anonymous>) at iCloud.get (/Users/derrickw/VS Code Projects/Find My (Notifications)/node_modules/apple-icloud/resources/apps/FindMe.js:13:26) at Object.main.<computed> [as get] (/Users/derrickw/VS Code Projects/Find My (Notifications)/node_modules/apple-icloud/resources/helper.js:84:27) at getDevices (/Users/derrickw/VS Code Projects/Find My (Notifications)/renderer.js:31:28) at processTicksAndRejections (node:internal/process/task_queues:96:5) {stack: 'TypeError: Cannot read properties of undefine…ions (node:internal/process/task_queues:96:5)', message: 'Cannot read properties of undefined (reading 'findme')'}

Notes Content

I'm able to successfully retrieve the note I'm interested in and can see the data that's returned, but I can't figure out how to get the full content of the note as plain text. I can see a "data" field containing a object, which I am guessing is the note's content, but I haven't been able to decode it. Do you know how to do that?

{
  recordName: 'd8896984-a0da-462f-bf24-8f2cf2431e75',
  recordType: 'Note',
  pluginFields: {},
  recordChangeTag: 'b',
  created: {
    timestamp: 1571592174025,
    userRecordName: '_cc9a65f4e232bd8c509b7ca174c9e6ec',
    deviceID: '2'
  },
  modified: {
    timestamp: 1571592924524,
    userRecordName: '_cc9a65f4e232bd8c509b7ca174c9e6ec',
    deviceID: '2'
  },
  deleted: false,
  displayedHostname: 'www.icloud.com',
  shortGUID: '07IwT2yxt2XNT5aXrvBI6hobg',
  title: 'Test Note',
  snippet: 'Hello World.',
  data: <Buffer 78 9c 4d cf cd 6a c2 40 10 07 f0 dd 7c 8e a3 b5 d3 0d 7a 58 84 46 73 09 82 3d f5 a2 97 82 20 94 1e 3c 59 fa 02 e6 16 08 68 8e 3e 8e f8 30 5e 7c 88 b6 ... 178 more bytes>,
  parent: {
    recordName: 'DefaultFolder-CloudKit',
    action: 'VALIDATE',
    zoneID: {
      zoneName: 'Notes',
      ownerRecordName: '_cc9a65f4e232bd8c509b7ca174c9e6ec',
      zoneType: 'REGULAR_CUSTOM_ZONE'
    }
  },
  ModificationDate: 1571592921648
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.