homebridge-icloud-smtp 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 iHaring
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,126 @@
1
+ # homebridge-icloud-smtp
2
+
3
+ Homebridge dynamic platform plugin that creates virtual switches. Turning a switch on sends an email through iCloud SMTP.
4
+
5
+ ## Features
6
+
7
+ - Create one or more HomeKit switches in Homebridge.
8
+ - Send an email when a switch is turned on.
9
+ - Per-switch cooldown to prevent repeated sends.
10
+ - Per-switch retry logic for temporary SMTP failures.
11
+ - Optional debug logging.
12
+
13
+ ## Requirements
14
+
15
+ - Homebridge `^1.8.0 || ^2.0.0`
16
+ - Node.js `^22.10.0 || ^24.0.0`
17
+ - An iCloud account
18
+ - An Apple app-specific password (required for SMTP)
19
+
20
+ ## Install
21
+
22
+ Install from npm:
23
+
24
+ ```bash
25
+ sudo npm install -g homebridge-icloud-smtp
26
+ ```
27
+
28
+ Or install from the Homebridge UI:
29
+
30
+ 1. Open Homebridge UI.
31
+ 2. Go to **Plugins**.
32
+ 3. Search for `homebridge-icloud-smtp`.
33
+ 4. Install the plugin.
34
+
35
+ ## Configuration
36
+
37
+ Add the platform to your Homebridge config.
38
+
39
+ ```json
40
+ {
41
+ "platform": "ICloudSMTP",
42
+ "name": "iCloud SMTP",
43
+ "username": "you@icloud.com",
44
+ "password": "your-app-specific-password",
45
+ "debug": false,
46
+ "switches": [
47
+ {
48
+ "name": "Door Alert",
49
+ "to": "notify@example.com",
50
+ "subject": "Door Opened",
51
+ "body": "The front door was opened.",
52
+ "cooldown": 30,
53
+ "retries": 2
54
+ }
55
+ ]
56
+ }
57
+ ```
58
+
59
+ ### Option Reference
60
+
61
+ Platform options:
62
+
63
+ - `platform` (string, required): must be `ICloudSMTP`.
64
+ - `name` (string, recommended): display name in Homebridge.
65
+ - `username` (string, required): your iCloud email address.
66
+ - `password` (string, required): Apple app-specific password.
67
+ - `debug` (boolean, optional, default `false`): enable debug logs.
68
+ - `switches` (array, required): list of virtual mail switches.
69
+
70
+ Switch options:
71
+
72
+ - `name` (string, required): switch name shown in HomeKit.
73
+ - `to` (string, required): recipient email address.
74
+ - `subject` (string, required): email subject.
75
+ - `body` (string, required): plain-text email body.
76
+ - `cooldown` (number, optional, default `30`): minimum seconds between sends.
77
+ - `retries` (number, optional, default `2`): retry count after the first failed send.
78
+
79
+ ## Apple App-Specific Password
80
+
81
+ Apple requires app-specific passwords for third-party SMTP access.
82
+
83
+ Steps:
84
+
85
+ 1. Sign in at [account.apple.com](https://account.apple.com/).
86
+ 2. Open **Sign-In and Security**.
87
+ 3. Select **App-Specific Passwords**.
88
+ 4. Generate a new app-specific password.
89
+ 5. Use that value as the plugin `password`.
90
+
91
+ Reference: [Apple Support - Use app-specific passwords](https://support.apple.com/en-us/102654)
92
+
93
+ ## How It Works
94
+
95
+ - The plugin registers a dynamic platform (`ICloudSMTP`).
96
+ - Each configured switch appears as a HomeKit switch accessory.
97
+ - Turning a switch on sends one email and then resets the switch to off.
98
+ - Emails are sent through `smtp.mail.me.com:587` using STARTTLS (`secure: false`).
99
+
100
+ ## Troubleshooting
101
+
102
+ - **"Missing iCloud credentials"**
103
+ - Ensure `username` and `password` are set in config.
104
+ - **"No switches defined"**
105
+ - Add at least one entry in `switches`.
106
+ - **Authentication failures**
107
+ - Verify you are using an app-specific password, not your Apple account password.
108
+ - Regenerate the app-specific password and update config.
109
+ - **Switch turns on but no mail arrives**
110
+ - Check Homebridge logs for retry/failure messages.
111
+ - Verify recipient address and spam/junk folders.
112
+
113
+ ## Security Notes
114
+
115
+ - Treat your app-specific password as a secret.
116
+ - Do not commit credentials to Git.
117
+ - Prefer Homebridge UI secret fields or environment-based secret handling where available.
118
+
119
+ ## Support
120
+
121
+ - Create an issue: https://github.com/iHaring/homebridge-icloud-smtp/issues
122
+ - Include your Homebridge version, Node.js version, and relevant log output.
123
+
124
+ ## License
125
+
126
+ MIT
@@ -0,0 +1,67 @@
1
+ {
2
+ "pluginAlias": "ICloudSMTP",
3
+ "pluginType": "platform",
4
+ "singular": false,
5
+ "headerDisplay": "Send emails via iCloud SMTP when switches are triggered",
6
+ "schema": {
7
+ "type": "object",
8
+ "required": [
9
+ "username",
10
+ "password"
11
+ ],
12
+ "properties": {
13
+ "username": {
14
+ "title": "Enter your iCloud email address to send the emails from.",
15
+ "type": "string",
16
+ "required": true,
17
+ "format": "email",
18
+ "description": "Use the same email address that is used to login on iCloud.com when you generate the app-specific password."
19
+ },
20
+ "password": {
21
+ "title": "app-specific password",
22
+ "type": "string",
23
+ "description": "Generate an app-specific password on account.apple.com. Select Generate an app-specific password, then follow the steps on your screen."
24
+ },
25
+ "debug": {
26
+ "type": "boolean",
27
+ "default": false
28
+ },
29
+ "switches": {
30
+ "type": "array",
31
+ "minItems": 1,
32
+ "items": {
33
+ "type": "object",
34
+ "required": [
35
+ "name",
36
+ "to",
37
+ "subject",
38
+ "body"
39
+ ],
40
+ "properties": {
41
+ "name": {
42
+ "type": "string"
43
+ },
44
+ "to": {
45
+ "type": "string",
46
+ "format": "email"
47
+ },
48
+ "subject": {
49
+ "type": "string"
50
+ },
51
+ "body": {
52
+ "type": "string"
53
+ },
54
+ "cooldown": {
55
+ "type": "number",
56
+ "default": 30
57
+ },
58
+ "retries": {
59
+ "type": "number",
60
+ "default": 2
61
+ }
62
+ }
63
+ }
64
+ }
65
+ }
66
+ }
67
+ }
package/index.js ADDED
@@ -0,0 +1,9 @@
1
+
2
+ const PLATFORM_NAME = 'ICloudSMTP';
3
+ const PLUGIN_NAME = 'homebridge-icloud-smtp';
4
+
5
+ const { ICloudSMTPPlatform } = require('./platform');
6
+
7
+ module.exports = (api) => {
8
+ api.registerPlatform(PLUGIN_NAME, PLATFORM_NAME, ICloudSMTPPlatform);
9
+ };
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "homebridge-icloud-smtp",
3
+ "displayName": "iCloud SMTP Switches",
4
+ "type": "commonjs",
5
+ "version": "0.0.7",
6
+ "description": "Virtual Switches to be able to send emails from Homebrige.",
7
+ "author": "liamharing",
8
+ "license": "MIT",
9
+ "homepage": "https://github.com/iHaring/homebridge-icloud-smtp#readme",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "https://github.com/iHaring/homebridge-icloud-smtp.git"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/iHaring/homebridge-icloud-smtp/issues"
16
+ },
17
+ "keywords": [
18
+ "homebridge",
19
+ "homebridge-plugin",
20
+ "plugin",
21
+ "homebridge plugin",
22
+ "virtual",
23
+ "virtual accessory",
24
+ "virtual accessories",
25
+ "switch",
26
+ "virtual switch",
27
+ "delay",
28
+ "timer",
29
+ "dummy"
30
+ ],
31
+ "main": "./index.js",
32
+ "files": [
33
+ "index.js",
34
+ "platform.js",
35
+ "switchAccessory.js",
36
+ "config.schema.json",
37
+ "LICENSE",
38
+ "README.md"
39
+ ],
40
+ "engines": {
41
+ "node": "^22.10.0 || ^24.0.0",
42
+ "homebridge": "^1.8.0 || ^2.0.0"
43
+ },
44
+ "dependencies": {
45
+ "nodemailer": "^8.0.7"
46
+ }
47
+ }
package/platform.js ADDED
@@ -0,0 +1,124 @@
1
+ const { MailSwitchAccessory } = require('./switchAccessory');
2
+
3
+ const PLATFORM_NAME = 'ICloudSMTP';
4
+ const PLUGIN_NAME = 'homebridge-icloud-smtp';
5
+
6
+ class ICloudSMTPPlatform {
7
+ constructor(log, config, api) {
8
+ this.log = log;
9
+ this.config = config || {};
10
+ this.api = api;
11
+ this.accessories = [];
12
+
13
+ // DEBUG FLAG (from config)
14
+ this.debug = this.config.debug === true;
15
+
16
+ // Debug logger helper
17
+ this.dlog = (msg, ...args) => {
18
+ if (this.debug) {
19
+ this.log.info(`[DEBUG] ${msg}`, ...args);
20
+ }
21
+ };
22
+
23
+ this.queue = {
24
+ promise: Promise.resolve(),
25
+ add: function (task) {
26
+ this.promise = this.promise
27
+ .then(() => task())
28
+ .catch(err => {
29
+ console.error('Queue error:', err);
30
+ });
31
+ return this.promise;
32
+ }
33
+ };
34
+
35
+ try {
36
+ this.validateConfig();
37
+ } catch (err) {
38
+ this.log.error(err.message);
39
+ throw err;
40
+ }
41
+
42
+ this.log.info('ICloudSMTP initialized');
43
+ this.dlog('Platform constructor complete');
44
+
45
+ api.on('didFinishLaunching', () => this.init());
46
+ }
47
+
48
+ validateConfig() {
49
+ this.dlog('Validating config...');
50
+
51
+ if (!this.config.username || !this.config.password) {
52
+ throw new Error('Missing iCloud credentials');
53
+ }
54
+
55
+ if (!Array.isArray(this.config.switches) || this.config.switches.length === 0) {
56
+ throw new Error('No switches defined');
57
+ }
58
+
59
+ this.dlog(`Config valid. Switch count: ${this.config.switches.length}`);
60
+ }
61
+
62
+ configureAccessory(accessory) {
63
+ this.dlog(`Restoring accessory from cache: ${accessory.displayName}`);
64
+ this.accessories.push(accessory);
65
+ }
66
+
67
+ init() {
68
+ this.dlog('init() called');
69
+
70
+ const validUUIDs = new Set();
71
+
72
+ for (const sw of this.config.switches || []) {
73
+ const uuid = this.api.hap.uuid.generate(`${sw.name}-${sw.to}`);
74
+ validUUIDs.add(uuid);
75
+
76
+ let accessory = this.accessories.find(a => a.UUID === uuid);
77
+
78
+ if (!accessory) {
79
+ accessory = new this.api.platformAccessory(sw.name, uuid);
80
+
81
+ this.api.registerPlatformAccessories(
82
+ PLUGIN_NAME,
83
+ PLATFORM_NAME,
84
+ [accessory]
85
+ );
86
+
87
+ this.log.info(`Adding new accessory: ${sw.name}`);
88
+ this.dlog(`Created new accessory UUID: ${uuid}`);
89
+ } else {
90
+ this.log.info(`Restoring existing accessory: ${sw.name}`);
91
+ this.dlog(`Matched cached UUID: ${uuid}`);
92
+ }
93
+
94
+ accessory.context.config = sw;
95
+
96
+ new MailSwitchAccessory(
97
+ this.log,
98
+ this.api,
99
+ accessory,
100
+ sw,
101
+ this.config,
102
+ this.queue
103
+ );
104
+ }
105
+
106
+ // Remove stale accessories
107
+ for (const accessory of this.accessories) {
108
+ if (!validUUIDs.has(accessory.UUID)) {
109
+ this.log.info(`Removing stale accessory: ${accessory.displayName}`);
110
+ this.dlog(`Unregistering UUID: ${accessory.UUID}`);
111
+
112
+ this.api.unregisterPlatformAccessories(
113
+ PLUGIN_NAME,
114
+ PLATFORM_NAME,
115
+ [accessory]
116
+ );
117
+ }
118
+ }
119
+
120
+ this.dlog('init() complete');
121
+ }
122
+ }
123
+
124
+ module.exports = { ICloudSMTPPlatform };
@@ -0,0 +1,95 @@
1
+
2
+ const nodemailer = require('nodemailer');
3
+
4
+ class MailSwitchAccessory {
5
+ constructor(log, api, accessory, swConfig, platformConfig, queue) {
6
+ this.log = log;
7
+ this.api = api;
8
+ this.accessory = accessory;
9
+ this.swConfig = swConfig;
10
+ this.platformConfig = platformConfig;
11
+ this.queue = queue;
12
+ this.lastSent = 0;
13
+
14
+ this.service =
15
+ accessory.getService(api.hap.Service.Switch) ||
16
+ accessory.addService(api.hap.Service.Switch, swConfig.name);
17
+
18
+ accessory.getService(api.hap.Service.AccessoryInformation)
19
+ .setCharacteristic(api.hap.Characteristic.Manufacturer, 'Custom')
20
+ .setCharacteristic(api.hap.Characteristic.Model, 'iCloud SMTP Switch')
21
+ .setCharacteristic(api.hap.Characteristic.SerialNumber, swConfig.name);
22
+
23
+ this.service
24
+ .getCharacteristic(api.hap.Characteristic.On)
25
+ .onSet(this.setState.bind(this))
26
+ .onGet(() => false);
27
+
28
+ this.transporter = nodemailer.createTransport({
29
+ host: 'smtp.mail.me.com',
30
+ port: 587,
31
+ secure: false,
32
+ auth: {
33
+ user: platformConfig.username,
34
+ pass: platformConfig.password
35
+ }
36
+ });
37
+ }
38
+
39
+ logPrefix() {
40
+ return `[${this.swConfig.name}]`;
41
+ }
42
+
43
+ async setState(value) {
44
+ if (!value) return;
45
+
46
+ const now = Date.now();
47
+ const cooldownMs = (this.swConfig.cooldown || 30) * 1000;
48
+
49
+ this.log.info(this.logPrefix() + ' triggered');
50
+
51
+ if (now - this.lastSent < cooldownMs) {
52
+ this.log.warn(this.logPrefix() + ' cooldown active');
53
+ return this.reset();
54
+ }
55
+
56
+ this.lastSent = now;
57
+
58
+ await this.queue.add(() => this.sendWithRetry());
59
+
60
+ this.reset();
61
+ }
62
+
63
+ async sendWithRetry() {
64
+ const retries = this.swConfig.retries || 2;
65
+
66
+ for (let i = 1; i <= retries + 1; i++) {
67
+ try {
68
+ this.log.info(this.logPrefix() + ' sending attempt ' + i);
69
+
70
+ await this.transporter.sendMail({
71
+ from: this.platformConfig.username,
72
+ to: this.swConfig.to,
73
+ subject: this.swConfig.subject,
74
+ text: this.swConfig.body
75
+ });
76
+
77
+ this.log.info(this.logPrefix() + ' email sent');
78
+ return;
79
+ } catch (e) {
80
+ this.log.warn(this.logPrefix() + ' attempt ' + i + ' failed');
81
+ if (i > retries) {
82
+ this.log.error(this.logPrefix() + ' all retries failed');
83
+ }
84
+ }
85
+ }
86
+ }
87
+
88
+ reset() {
89
+ setTimeout(() => {
90
+ this.service.updateCharacteristic(this.api.hap.Characteristic.On, false);
91
+ }, 500);
92
+ }
93
+ }
94
+
95
+ module.exports = { MailSwitchAccessory };