@cakemail-org/cakemail-cli 1.5.0 → 2.0.0
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/.claude/settings.local.json +12 -0
- package/.env.example +40 -0
- package/.env.test.example +45 -0
- package/CHANGELOG.md +1031 -0
- package/README.md +319 -15
- package/audit-formats.js +128 -0
- package/cakemail.rb +20 -0
- package/dist/cli.js +27 -10
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +2 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +16 -6
- package/dist/client.js.map +1 -1
- package/dist/commands/account.js +1 -1
- package/dist/commands/account.js.map +1 -1
- package/dist/commands/attributes.js +1 -1
- package/dist/commands/attributes.js.map +1 -1
- package/dist/commands/campaigns.d.ts.map +1 -1
- package/dist/commands/campaigns.js +103 -8
- package/dist/commands/campaigns.js.map +1 -1
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +63 -4
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/contacts.d.ts.map +1 -1
- package/dist/commands/contacts.js +91 -12
- package/dist/commands/contacts.js.map +1 -1
- package/dist/commands/emails.js +1 -1
- package/dist/commands/emails.js.map +1 -1
- package/dist/commands/interests.d.ts +5 -0
- package/dist/commands/interests.d.ts.map +1 -0
- package/dist/commands/interests.js +172 -0
- package/dist/commands/interests.js.map +1 -0
- package/dist/commands/lists.d.ts.map +1 -1
- package/dist/commands/lists.js +6 -8
- package/dist/commands/lists.js.map +1 -1
- package/dist/commands/logs.d.ts +5 -0
- package/dist/commands/logs.d.ts.map +1 -0
- package/dist/commands/logs.js +237 -0
- package/dist/commands/logs.js.map +1 -0
- package/dist/commands/reports.js +1 -1
- package/dist/commands/reports.js.map +1 -1
- package/dist/commands/segments.js +1 -1
- package/dist/commands/segments.js.map +1 -1
- package/dist/commands/senders.d.ts.map +1 -1
- package/dist/commands/senders.js +11 -8
- package/dist/commands/senders.js.map +1 -1
- package/dist/commands/suppressed.js +1 -1
- package/dist/commands/suppressed.js.map +1 -1
- package/dist/commands/tags.d.ts +5 -0
- package/dist/commands/tags.d.ts.map +1 -0
- package/dist/commands/tags.js +124 -0
- package/dist/commands/tags.js.map +1 -0
- package/dist/commands/templates.js +1 -1
- package/dist/commands/templates.js.map +1 -1
- package/dist/commands/transactional-templates.d.ts +5 -0
- package/dist/commands/transactional-templates.d.ts.map +1 -0
- package/dist/commands/transactional-templates.js +354 -0
- package/dist/commands/transactional-templates.js.map +1 -0
- package/dist/commands/webhooks.js +1 -1
- package/dist/commands/webhooks.js.map +1 -1
- package/dist/utils/auth.d.ts +8 -1
- package/dist/utils/auth.d.ts.map +1 -1
- package/dist/utils/auth.js +39 -11
- package/dist/utils/auth.js.map +1 -1
- package/dist/utils/config-file.d.ts +7 -0
- package/dist/utils/config-file.d.ts.map +1 -1
- package/dist/utils/config-file.js +15 -0
- package/dist/utils/config-file.js.map +1 -1
- package/dist/utils/config.d.ts +2 -0
- package/dist/utils/config.d.ts.map +1 -1
- package/dist/utils/config.js +12 -4
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/errors.js +1 -1
- package/dist/utils/errors.js.map +1 -1
- package/dist/utils/list-defaults.d.ts +33 -0
- package/dist/utils/list-defaults.d.ts.map +1 -0
- package/dist/utils/list-defaults.js +52 -0
- package/dist/utils/list-defaults.js.map +1 -0
- package/dist/utils/output.d.ts.map +1 -1
- package/dist/utils/output.js +36 -13
- package/dist/utils/output.js.map +1 -1
- package/dist/utils/progress.d.ts.map +1 -1
- package/dist/utils/progress.js +32 -4
- package/dist/utils/progress.js.map +1 -1
- package/dist/utils/spinner.d.ts +17 -0
- package/dist/utils/spinner.d.ts.map +1 -0
- package/dist/utils/spinner.js +43 -0
- package/dist/utils/spinner.js.map +1 -0
- package/docs/DOCUMENTATION-STANDARD.md +1068 -0
- package/docs/README.md +161 -0
- package/docs/developer/ARCHITECTURE.md +516 -0
- package/docs/developer/AUTH.md +204 -0
- package/docs/developer/CONTRIBUTING.md +227 -0
- package/docs/developer/DOCUMENTATION_SUMMARY.md +346 -0
- package/docs/developer/PROJECT_INDEX.md +365 -0
- package/docs/planning/API_COVERAGE.md +1045 -0
- package/docs/planning/BACKLOG.md +1159 -0
- package/docs/planning/PROFILE_SYSTEM_TASKS.md +287 -0
- package/docs/planning/UX_IMPLEMENTATION_PLAN.md +691 -0
- package/docs/planning/archive/RELEASE_CHECKLIST_v1.3.0.md +332 -0
- package/docs/planning/archive/RELEASE_v1.3.0.md +428 -0
- package/docs/planning/archive/cakemail-cli-ux-improvements.md +438 -0
- package/docs/planning/cakemail-profile-system-plan.md +1121 -0
- package/docs/testing/AI_USER_SIMULATION_DESIGN.md +1342 -0
- package/docs/testing/KENOGAMI_BIDIRECTIONAL_FLOW.md +1517 -0
- package/docs/testing/KENOGAMI_TRUTH_RECONCILIATION_SYSTEM.md +1369 -0
- package/docs/user-manual/.obsidian/app.json +1 -0
- package/docs/user-manual/.obsidian/appearance.json +1 -0
- package/docs/user-manual/.obsidian/core-plugins.json +33 -0
- package/docs/user-manual/.obsidian/workspace.json +167 -0
- package/docs/user-manual/01-getting-started/01-installation.md +214 -0
- package/docs/user-manual/01-getting-started/02-quick-start.md +432 -0
- package/docs/user-manual/01-getting-started/03-authentication.md +448 -0
- package/docs/user-manual/01-getting-started/04-configuration.md +430 -0
- package/docs/user-manual/01-getting-started/05-output-formats.md +447 -0
- package/docs/user-manual/02-core-concepts/01-accounts.md +514 -0
- package/docs/user-manual/02-core-concepts/02-profile-system.md +771 -0
- package/docs/user-manual/02-core-concepts/03-smart-defaults.md +485 -0
- package/docs/user-manual/02-core-concepts/04-authentication-methods.md +435 -0
- package/docs/user-manual/02-core-concepts/05-pagination-filtering.md +600 -0
- package/docs/user-manual/02-core-concepts/06-error-handling.md +718 -0
- package/docs/user-manual/02-core-concepts/07-api-coverage.md +483 -0
- package/docs/user-manual/03-email-operations/01-senders.md +490 -0
- package/docs/user-manual/03-email-operations/02-templates.md +444 -0
- package/docs/user-manual/03-email-operations/03-transactional-emails.md +706 -0
- package/docs/user-manual/03-email-operations/04-email-tracking.md +407 -0
- package/docs/user-manual/04-campaign-management/01-campaigns-basics.md +394 -0
- package/docs/user-manual/04-campaign-management/02-campaign-scheduling.md +630 -0
- package/docs/user-manual/04-campaign-management/03-campaign-testing.md +997 -0
- package/docs/user-manual/04-campaign-management/04-campaign-lifecycle.md +709 -0
- package/docs/user-manual/04-campaign-management/05-campaign-links.md +934 -0
- package/docs/user-manual/05-contact-management/01-lists.md +836 -0
- package/docs/user-manual/05-contact-management/02-contacts.md +1035 -0
- package/docs/user-manual/05-contact-management/03-custom-attributes.md +788 -0
- package/docs/user-manual/05-contact-management/04-segments.md +1028 -0
- package/docs/user-manual/05-contact-management/05-contact-import-export.md +1031 -0
- package/docs/user-manual/06-analytics-reporting/01-campaign-analytics.md +867 -0
- package/docs/user-manual/06-analytics-reporting/02-account-reports.md +227 -0
- package/docs/user-manual/07-integrations/01-webhooks-integration.md +259 -0
- package/docs/user-manual/07-integrations/02-automation.md +326 -0
- package/docs/user-manual/08-advanced-usage/01-scripting-patterns.md +672 -0
- package/docs/user-manual/08-advanced-usage/02-bulk-operations.md +932 -0
- package/docs/user-manual/08-advanced-usage/03-ci-cd-integration.md +892 -0
- package/docs/user-manual/08-advanced-usage/04-performance-optimization.md +766 -0
- package/docs/user-manual/09-command-reference/01-config.md +776 -0
- package/docs/user-manual/09-command-reference/02-account.md +652 -0
- package/docs/user-manual/09-command-reference/03-lists.md +958 -0
- package/docs/user-manual/09-command-reference/04-contacts.md +1408 -0
- package/docs/user-manual/09-command-reference/05-attributes.md +617 -0
- package/docs/user-manual/09-command-reference/06-segments.md +894 -0
- package/docs/user-manual/09-command-reference/07-senders.md +803 -0
- package/docs/user-manual/09-command-reference/08-templates.md +818 -0
- package/docs/user-manual/09-command-reference/09-campaigns.md +1250 -0
- package/docs/user-manual/09-command-reference/10-emails.md +807 -0
- package/docs/user-manual/09-command-reference/11-reports.md +1135 -0
- package/docs/user-manual/09-command-reference/12-webhooks.md +773 -0
- package/docs/user-manual/09-command-reference/13-suppressed.md +797 -0
- package/docs/user-manual/09-command-reference/14-interests.md +630 -0
- package/docs/user-manual/09-command-reference/15-tags.md +584 -0
- package/docs/user-manual/09-command-reference/16-logs.md +656 -0
- package/docs/user-manual/09-command-reference/17-transactional-templates.md +850 -0
- package/docs/user-manual/10-troubleshooting/01-common-errors.md +457 -0
- package/docs/user-manual/10-troubleshooting/02-authentication-issues.md +558 -0
- package/docs/user-manual/10-troubleshooting/03-connection-problems.md +634 -0
- package/docs/user-manual/10-troubleshooting/04-debugging.md +725 -0
- package/docs/user-manual/11-appendix/04-faq.md +484 -0
- package/docs/user-manual/11-appendix/05-glossary.md +250 -0
- package/docs/user-manual/README.md +0 -0
- package/package.json +13 -47
- package/src/cli.ts +125 -0
- package/src/client.ts +16 -0
- package/src/commands/account.ts +267 -0
- package/src/commands/accounts.ts +78 -0
- package/src/commands/actions.ts +249 -0
- package/src/commands/attributes.ts +139 -0
- package/src/commands/campaign-blueprints.ts +106 -0
- package/src/commands/campaigns.ts +469 -0
- package/src/commands/config.ts +77 -0
- package/src/commands/contacts.ts +612 -0
- package/src/commands/custom-attributes.ts +127 -0
- package/src/commands/dkims.ts +117 -0
- package/src/commands/domains.ts +82 -0
- package/src/commands/email-apis.ts +569 -0
- package/src/commands/emails.ts +197 -0
- package/src/commands/forms.ts +283 -0
- package/src/commands/interests.ts +155 -0
- package/src/commands/links.ts +38 -0
- package/src/commands/lists.ts +406 -0
- package/src/commands/logos.ts +71 -0
- package/src/commands/logs.ts +386 -0
- package/src/commands/reports.ts +306 -0
- package/src/commands/segments.ts +158 -0
- package/src/commands/senders.ts +204 -0
- package/src/commands/sub-accounts.ts +271 -0
- package/src/commands/suppressed-emails.ts +234 -0
- package/src/commands/suppressed.ts +198 -0
- package/src/commands/system-emails.ts +85 -0
- package/src/commands/tags.ts +146 -0
- package/src/commands/tasks.ts +116 -0
- package/src/commands/templates.ts +189 -0
- package/src/commands/tokens.ts +83 -0
- package/src/commands/transactional-emails.ts +374 -0
- package/src/commands/transactional-templates.ts +385 -0
- package/src/commands/users.ts +506 -0
- package/src/commands/webhooks.ts +172 -0
- package/src/commands/workflow-blueprints.ts +123 -0
- package/src/commands/workflows.ts +265 -0
- package/src/types/profile.ts +93 -0
- package/src/utils/auth.ts +272 -0
- package/src/utils/config-file.ts +96 -0
- package/src/utils/config.ts +134 -0
- package/src/utils/confirm.ts +32 -0
- package/src/utils/defaults.ts +99 -0
- package/src/utils/errors.ts +116 -0
- package/src/utils/interactive.ts +91 -0
- package/src/utils/list-defaults.ts +74 -0
- package/src/utils/output.ts +190 -0
- package/src/utils/progress.ts +320 -0
- package/src/utils/spinner.ts +22 -0
- package/tests/IMPLEMENTATION_STATUS.md +258 -0
- package/tests/PTY_SETUP.md +118 -0
- package/tests/PTY_TESTING_GUIDE.md +507 -0
- package/tests/README.md +244 -0
- package/tests/fixtures/api-responses/campaigns.json +34 -0
- package/tests/fixtures/test-config.json +13 -0
- package/tests/helpers/cli-runner.ts +128 -0
- package/tests/helpers/mock-server.ts +301 -0
- package/tests/helpers/pty-runner.ts +181 -0
- package/tests/integration/campaigns-real-api.test.ts +196 -0
- package/tests/integration/setup-integration.ts +50 -0
- package/tests/pty/campaigns.test.ts +241 -0
- package/tests/setup.ts +34 -0
- package/tsconfig.json +15 -0
- package/vitest.config.ts +28 -0
|
@@ -0,0 +1,1035 @@
|
|
|
1
|
+
# Managing Contacts
|
|
2
|
+
|
|
3
|
+
Master individual contact management including adding, updating, searching, and tracking contact data.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Contact management allows you to:
|
|
8
|
+
- Add individual contacts to lists
|
|
9
|
+
- Update contact information
|
|
10
|
+
- Track custom attributes and preferences
|
|
11
|
+
- Search and filter contacts
|
|
12
|
+
- View contact engagement history
|
|
13
|
+
- Manage subscription status
|
|
14
|
+
- Delete contacts when needed
|
|
15
|
+
|
|
16
|
+
Every contact belongs to at least one list and can have standard fields (email, name) plus custom attributes for your specific data needs.
|
|
17
|
+
|
|
18
|
+
## Quick Start
|
|
19
|
+
|
|
20
|
+
### Add Your First Contact
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
$ cakemail contacts add 123 -e "john@example.com" -f "John" -l "Doe"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**Output:**
|
|
27
|
+
```
|
|
28
|
+
✓ Contact added successfully
|
|
29
|
+
|
|
30
|
+
ID: 501
|
|
31
|
+
Email: john@example.com
|
|
32
|
+
Name: John Doe
|
|
33
|
+
Status: subscribed
|
|
34
|
+
List: 123
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### View Contact Details
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
$ cakemail contacts get 123 501
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**Output:**
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"id": 501,
|
|
47
|
+
"email": "john@example.com",
|
|
48
|
+
"first_name": "John",
|
|
49
|
+
"last_name": "Doe",
|
|
50
|
+
"status": "subscribed",
|
|
51
|
+
"subscribed_on": "2024-03-15T10:30:00Z",
|
|
52
|
+
"last_bounce": null,
|
|
53
|
+
"bounces_count": 0,
|
|
54
|
+
"custom_attributes": {}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Contact Management Basics
|
|
59
|
+
|
|
60
|
+
### Add Contact
|
|
61
|
+
|
|
62
|
+
**Simple contact:**
|
|
63
|
+
```bash
|
|
64
|
+
$ cakemail contacts add 123 -e "jane@example.com"
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**With name:**
|
|
68
|
+
```bash
|
|
69
|
+
$ cakemail contacts add 123 \
|
|
70
|
+
-e "jane@example.com" \
|
|
71
|
+
-f "Jane" \
|
|
72
|
+
-l "Smith"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**With custom attributes:**
|
|
76
|
+
```bash
|
|
77
|
+
$ cakemail contacts add 123 \
|
|
78
|
+
-e "jane@example.com" \
|
|
79
|
+
-f "Jane" \
|
|
80
|
+
-l "Smith" \
|
|
81
|
+
-d '{"plan":"premium","signup_date":"2024-03-15","is_vip":true}'
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**Auto-detect list:**
|
|
85
|
+
```bash
|
|
86
|
+
# If you have only one list, list ID is optional
|
|
87
|
+
$ cakemail contacts add -e "new@example.com" -f "New" -l "User"
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**Output:**
|
|
91
|
+
```
|
|
92
|
+
✓ Auto-detected list: 123 (Newsletter Subscribers)
|
|
93
|
+
✓ Contact added successfully
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### View Contact
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
$ cakemail contacts get 123 501
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**Extract specific fields:**
|
|
103
|
+
```bash
|
|
104
|
+
$ cakemail contacts get 123 501 -f json | jq '{email, status, custom_attributes}'
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**Output:**
|
|
108
|
+
```json
|
|
109
|
+
{
|
|
110
|
+
"email": "john@example.com",
|
|
111
|
+
"status": "subscribed",
|
|
112
|
+
"custom_attributes": {
|
|
113
|
+
"plan": "premium",
|
|
114
|
+
"signup_date": "2024-03-15",
|
|
115
|
+
"is_vip": true
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Update Contact
|
|
121
|
+
|
|
122
|
+
**Update name:**
|
|
123
|
+
```bash
|
|
124
|
+
$ cakemail contacts update 123 501 -f "Jonathan" -l "Doe"
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**Update email:**
|
|
128
|
+
```bash
|
|
129
|
+
$ cakemail contacts update 123 501 -e "newemail@example.com"
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**Update custom attributes:**
|
|
133
|
+
```bash
|
|
134
|
+
$ cakemail contacts update 123 501 -d '{"plan":"enterprise","is_vip":true}'
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
**Update multiple fields:**
|
|
138
|
+
```bash
|
|
139
|
+
$ cakemail contacts update 123 501 \
|
|
140
|
+
-f "Jonathan" \
|
|
141
|
+
-l "Doe" \
|
|
142
|
+
-e "j.doe@example.com" \
|
|
143
|
+
-d '{"plan":"enterprise","lifetime_value":599.99}'
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Delete Contact
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
$ cakemail contacts delete 123 501
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
**Output:**
|
|
153
|
+
```
|
|
154
|
+
⚠ Delete contact 501?
|
|
155
|
+
|
|
156
|
+
The following will happen:
|
|
157
|
+
• Contact will be permanently deleted
|
|
158
|
+
• All engagement history lost
|
|
159
|
+
• Cannot be recovered
|
|
160
|
+
|
|
161
|
+
Type 'yes' to confirm: yes
|
|
162
|
+
|
|
163
|
+
✓ Contact 501 deleted
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
**Force delete (skip confirmation):**
|
|
167
|
+
```bash
|
|
168
|
+
$ cakemail contacts delete 123 501 --force
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Searching Contacts
|
|
172
|
+
|
|
173
|
+
### List All Contacts
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
$ cakemail contacts list 123
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
**Output:**
|
|
180
|
+
```
|
|
181
|
+
┌──────┬────────────────────────┬───────────────┬────────────┬─────────────────────┐
|
|
182
|
+
│ ID │ Email │ Name │ Status │ Subscribed │
|
|
183
|
+
├──────┼────────────────────────┼───────────────┼────────────┼─────────────────────┤
|
|
184
|
+
│ 501 │ john@example.com │ John Doe │ subscribed │ 2024-01-15 10:30:00 │
|
|
185
|
+
│ 502 │ jane@example.com │ Jane Smith │ subscribed │ 2024-01-16 14:20:00 │
|
|
186
|
+
│ 503 │ bob@example.com │ Bob Johnson │ subscribed │ 2024-01-17 09:15:00 │
|
|
187
|
+
└──────┴────────────────────────┴───────────────┴────────────┴─────────────────────┘
|
|
188
|
+
|
|
189
|
+
Total: 3 contacts
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Search by Email
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
$ cakemail contacts list 123 --filter "email==john@example.com"
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Search by Status
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
# Only subscribed
|
|
202
|
+
$ cakemail contacts list 123 --filter "status==subscribed"
|
|
203
|
+
|
|
204
|
+
# Unsubscribed contacts
|
|
205
|
+
$ cakemail contacts list 123 --filter "status==unsubscribed"
|
|
206
|
+
|
|
207
|
+
# Bounced contacts
|
|
208
|
+
$ cakemail contacts list 123 --filter "status==bounced"
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Search by Name
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
$ cakemail contacts list 123 --filter "first_name==John"
|
|
215
|
+
$ cakemail contacts list 123 --filter "last_name==Doe"
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Search by Custom Attributes
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
# Premium plan users
|
|
222
|
+
$ cakemail contacts list 123 --filter "custom_attributes.plan==premium"
|
|
223
|
+
|
|
224
|
+
# VIP contacts
|
|
225
|
+
$ cakemail contacts list 123 --filter "custom_attributes.is_vip==true"
|
|
226
|
+
|
|
227
|
+
# Recent signups
|
|
228
|
+
$ cakemail contacts list 123 --filter "custom_attributes.signup_date>=2024-03-01"
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Combine Filters
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
# Premium VIP subscribers
|
|
235
|
+
$ cakemail contacts list 123 --filter "status==subscribed;custom_attributes.plan==premium;custom_attributes.is_vip==true"
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Pagination
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
# First 100 contacts
|
|
242
|
+
$ cakemail contacts list 123 --limit 100
|
|
243
|
+
|
|
244
|
+
# Next 100 (page 2)
|
|
245
|
+
$ cakemail contacts list 123 --limit 100 --page 2
|
|
246
|
+
|
|
247
|
+
# Large list pagination
|
|
248
|
+
$ cakemail contacts list 123 --limit 500 --page 5
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Sort Results
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
# Sort by email
|
|
255
|
+
$ cakemail contacts list 123 --sort "+email"
|
|
256
|
+
|
|
257
|
+
# Sort by subscription date (newest first)
|
|
258
|
+
$ cakemail contacts list 123 --sort "-subscribed_on"
|
|
259
|
+
|
|
260
|
+
# Sort by name
|
|
261
|
+
$ cakemail contacts list 123 --sort "+first_name,+last_name"
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
## Working with Custom Attributes
|
|
265
|
+
|
|
266
|
+
### View Contact Attributes
|
|
267
|
+
|
|
268
|
+
```bash
|
|
269
|
+
$ cakemail contacts get 123 501 -f json | jq '.custom_attributes'
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
**Output:**
|
|
273
|
+
```json
|
|
274
|
+
{
|
|
275
|
+
"plan": "premium",
|
|
276
|
+
"signup_date": "2024-03-15",
|
|
277
|
+
"lifetime_value": 299.99,
|
|
278
|
+
"is_vip": true,
|
|
279
|
+
"purchase_count": 5
|
|
280
|
+
}
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Add Attributes to Existing Contact
|
|
284
|
+
|
|
285
|
+
```bash
|
|
286
|
+
$ cakemail contacts update 123 501 -d '{"company":"Acme Corp","industry":"Technology"}'
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### Update Single Attribute
|
|
290
|
+
|
|
291
|
+
```bash
|
|
292
|
+
# Upgrade plan
|
|
293
|
+
$ cakemail contacts update 123 501 -d '{"plan":"enterprise"}'
|
|
294
|
+
|
|
295
|
+
# Increment purchase count (requires fetching first)
|
|
296
|
+
CURRENT=$(cakemail contacts get 123 501 -f json | jq -r '.custom_attributes.purchase_count')
|
|
297
|
+
NEW=$((CURRENT + 1))
|
|
298
|
+
cakemail contacts update 123 501 -d "{\"purchase_count\":$NEW}"
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### Remove Attribute
|
|
302
|
+
|
|
303
|
+
```bash
|
|
304
|
+
# Set to null to remove
|
|
305
|
+
$ cakemail contacts update 123 501 -d '{"old_field":null}'
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
## Contact Workflows
|
|
309
|
+
|
|
310
|
+
### Workflow 1: Add Contact with Complete Data
|
|
311
|
+
|
|
312
|
+
```bash
|
|
313
|
+
#!/bin/bash
|
|
314
|
+
# add-complete-contact.sh
|
|
315
|
+
|
|
316
|
+
LIST_ID=123
|
|
317
|
+
EMAIL="$1"
|
|
318
|
+
FIRST_NAME="$2"
|
|
319
|
+
LAST_NAME="$3"
|
|
320
|
+
PLAN="$4"
|
|
321
|
+
|
|
322
|
+
if [ -z "$EMAIL" ] || [ -z "$FIRST_NAME" ] || [ -z "$LAST_NAME" ]; then
|
|
323
|
+
echo "Usage: $0 <email> <first-name> <last-name> [plan]"
|
|
324
|
+
exit 1
|
|
325
|
+
fi
|
|
326
|
+
|
|
327
|
+
PLAN=${PLAN:-basic}
|
|
328
|
+
SIGNUP_DATE=$(date +%Y-%m-%d)
|
|
329
|
+
|
|
330
|
+
echo "Adding contact: $FIRST_NAME $LAST_NAME ($EMAIL)"
|
|
331
|
+
|
|
332
|
+
CONTACT=$(cakemail contacts add $LIST_ID \
|
|
333
|
+
-e "$EMAIL" \
|
|
334
|
+
-f "$FIRST_NAME" \
|
|
335
|
+
-l "$LAST_NAME" \
|
|
336
|
+
-d "{
|
|
337
|
+
\"plan\": \"$PLAN\",
|
|
338
|
+
\"signup_date\": \"$SIGNUP_DATE\",
|
|
339
|
+
\"source\": \"manual_import\",
|
|
340
|
+
\"is_vip\": false,
|
|
341
|
+
\"purchase_count\": 0
|
|
342
|
+
}" \
|
|
343
|
+
-f json)
|
|
344
|
+
|
|
345
|
+
CONTACT_ID=$(echo "$CONTACT" | jq -r '.id')
|
|
346
|
+
|
|
347
|
+
echo "✓ Contact added: ID $CONTACT_ID"
|
|
348
|
+
echo " Email: $EMAIL"
|
|
349
|
+
echo " Name: $FIRST_NAME $LAST_NAME"
|
|
350
|
+
echo " Plan: $PLAN"
|
|
351
|
+
echo " Signup: $SIGNUP_DATE"
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
**Usage:**
|
|
355
|
+
```bash
|
|
356
|
+
$ ./add-complete-contact.sh john@example.com John Doe premium
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### Workflow 2: Update Contact from Form Data
|
|
360
|
+
|
|
361
|
+
```bash
|
|
362
|
+
#!/bin/bash
|
|
363
|
+
# update-from-form.sh
|
|
364
|
+
|
|
365
|
+
LIST_ID=123
|
|
366
|
+
EMAIL="$1"
|
|
367
|
+
|
|
368
|
+
if [ -z "$EMAIL" ]; then
|
|
369
|
+
echo "Usage: $0 <email>"
|
|
370
|
+
exit 1
|
|
371
|
+
fi
|
|
372
|
+
|
|
373
|
+
# Find contact by email
|
|
374
|
+
CONTACTS=$(cakemail contacts list $LIST_ID --filter "email==$EMAIL" -f json)
|
|
375
|
+
CONTACT_ID=$(echo "$CONTACTS" | jq -r '.data[0].id')
|
|
376
|
+
|
|
377
|
+
if [ "$CONTACT_ID" == "null" ]; then
|
|
378
|
+
echo "Contact not found: $EMAIL"
|
|
379
|
+
exit 1
|
|
380
|
+
fi
|
|
381
|
+
|
|
382
|
+
echo "Found contact: $CONTACT_ID"
|
|
383
|
+
echo ""
|
|
384
|
+
|
|
385
|
+
# Prompt for updates
|
|
386
|
+
read -p "First Name (Enter to skip): " FIRST_NAME
|
|
387
|
+
read -p "Last Name (Enter to skip): " LAST_NAME
|
|
388
|
+
read -p "Company (Enter to skip): " COMPANY
|
|
389
|
+
read -p "Plan (basic/premium/enterprise, Enter to skip): " PLAN
|
|
390
|
+
|
|
391
|
+
# Build update command
|
|
392
|
+
CMD="cakemail contacts update $LIST_ID $CONTACT_ID"
|
|
393
|
+
|
|
394
|
+
if [ -n "$FIRST_NAME" ]; then
|
|
395
|
+
CMD="$CMD -f \"$FIRST_NAME\""
|
|
396
|
+
fi
|
|
397
|
+
|
|
398
|
+
if [ -n "$LAST_NAME" ]; then
|
|
399
|
+
CMD="$CMD -l \"$LAST_NAME\""
|
|
400
|
+
fi
|
|
401
|
+
|
|
402
|
+
# Build custom attributes JSON
|
|
403
|
+
ATTRS="{"
|
|
404
|
+
if [ -n "$COMPANY" ]; then
|
|
405
|
+
ATTRS="$ATTRS\"company\":\"$COMPANY\","
|
|
406
|
+
fi
|
|
407
|
+
if [ -n "$PLAN" ]; then
|
|
408
|
+
ATTRS="$ATTRS\"plan\":\"$PLAN\","
|
|
409
|
+
fi
|
|
410
|
+
ATTRS="${ATTRS%,}}"
|
|
411
|
+
|
|
412
|
+
if [ "$ATTRS" != "{}" ]; then
|
|
413
|
+
CMD="$CMD -d '$ATTRS'"
|
|
414
|
+
fi
|
|
415
|
+
|
|
416
|
+
# Execute update
|
|
417
|
+
eval $CMD
|
|
418
|
+
|
|
419
|
+
echo ""
|
|
420
|
+
echo "✓ Contact updated"
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
### Workflow 3: Find and Update Multiple Contacts
|
|
424
|
+
|
|
425
|
+
```bash
|
|
426
|
+
#!/bin/bash
|
|
427
|
+
# bulk-update-plan.sh
|
|
428
|
+
|
|
429
|
+
LIST_ID=123
|
|
430
|
+
OLD_PLAN="$1"
|
|
431
|
+
NEW_PLAN="$2"
|
|
432
|
+
|
|
433
|
+
if [ -z "$OLD_PLAN" ] || [ -z "$NEW_PLAN" ]; then
|
|
434
|
+
echo "Usage: $0 <old-plan> <new-plan>"
|
|
435
|
+
echo "Example: $0 basic premium"
|
|
436
|
+
exit 1
|
|
437
|
+
fi
|
|
438
|
+
|
|
439
|
+
echo "=== Bulk Plan Update ==="
|
|
440
|
+
echo "Updating contacts from $OLD_PLAN to $NEW_PLAN"
|
|
441
|
+
echo ""
|
|
442
|
+
|
|
443
|
+
# Find contacts with old plan
|
|
444
|
+
CONTACTS=$(cakemail contacts list $LIST_ID \
|
|
445
|
+
--filter "custom_attributes.plan==$OLD_PLAN" \
|
|
446
|
+
-f json | jq -r '.data[].id')
|
|
447
|
+
|
|
448
|
+
COUNT=$(echo "$CONTACTS" | wc -l)
|
|
449
|
+
|
|
450
|
+
if [ -z "$CONTACTS" ]; then
|
|
451
|
+
echo "No contacts found with plan: $OLD_PLAN"
|
|
452
|
+
exit 0
|
|
453
|
+
fi
|
|
454
|
+
|
|
455
|
+
echo "Found $COUNT contacts to update"
|
|
456
|
+
read -p "Continue? (yes/no): " CONFIRM
|
|
457
|
+
|
|
458
|
+
if [ "$CONFIRM" != "yes" ]; then
|
|
459
|
+
echo "Cancelled"
|
|
460
|
+
exit 0
|
|
461
|
+
fi
|
|
462
|
+
|
|
463
|
+
echo ""
|
|
464
|
+
|
|
465
|
+
# Update each contact
|
|
466
|
+
for CONTACT_ID in $CONTACTS; do
|
|
467
|
+
echo "Updating contact $CONTACT_ID..."
|
|
468
|
+
cakemail contacts update $LIST_ID $CONTACT_ID \
|
|
469
|
+
-d "{\"plan\":\"$NEW_PLAN\"}"
|
|
470
|
+
done
|
|
471
|
+
|
|
472
|
+
echo ""
|
|
473
|
+
echo "✓ Updated $COUNT contacts"
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
### Workflow 4: Contact Activity Report
|
|
477
|
+
|
|
478
|
+
```bash
|
|
479
|
+
#!/bin/bash
|
|
480
|
+
# contact-activity.sh
|
|
481
|
+
|
|
482
|
+
LIST_ID=123
|
|
483
|
+
CONTACT_ID=$1
|
|
484
|
+
|
|
485
|
+
if [ -z "$CONTACT_ID" ]; then
|
|
486
|
+
echo "Usage: $0 <contact-id>"
|
|
487
|
+
exit 1
|
|
488
|
+
fi
|
|
489
|
+
|
|
490
|
+
echo "=== Contact Activity Report ==="
|
|
491
|
+
echo ""
|
|
492
|
+
|
|
493
|
+
# Get contact details
|
|
494
|
+
CONTACT=$(cakemail contacts get $LIST_ID $CONTACT_ID -f json)
|
|
495
|
+
|
|
496
|
+
echo "Contact: $(echo "$CONTACT" | jq -r '.first_name') $(echo "$CONTACT" | jq -r '.last_name')"
|
|
497
|
+
echo "Email: $(echo "$CONTACT" | jq -r '.email')"
|
|
498
|
+
echo "Status: $(echo "$CONTACT" | jq -r '.status')"
|
|
499
|
+
echo ""
|
|
500
|
+
|
|
501
|
+
# Subscription info
|
|
502
|
+
echo "=== Subscription ==="
|
|
503
|
+
echo "Subscribed: $(echo "$CONTACT" | jq -r '.subscribed_on')"
|
|
504
|
+
echo "Last Modified: $(echo "$CONTACT" | jq -r '.last_modified')"
|
|
505
|
+
echo ""
|
|
506
|
+
|
|
507
|
+
# Engagement
|
|
508
|
+
echo "=== Engagement ==="
|
|
509
|
+
echo "Last Open: $(echo "$CONTACT" | jq -r '.last_open_date // "Never"')"
|
|
510
|
+
echo "Last Click: $(echo "$CONTACT" | jq -r '.last_click_date // "Never"')"
|
|
511
|
+
echo "Total Opens: $(echo "$CONTACT" | jq -r '.total_opens // 0')"
|
|
512
|
+
echo "Total Clicks: $(echo "$CONTACT" | jq -r '.total_clicks // 0')"
|
|
513
|
+
echo ""
|
|
514
|
+
|
|
515
|
+
# Bounces
|
|
516
|
+
BOUNCES=$(echo "$CONTACT" | jq -r '.bounces_count')
|
|
517
|
+
echo "=== Deliverability ==="
|
|
518
|
+
echo "Bounce Count: $BOUNCES"
|
|
519
|
+
if [ $BOUNCES -gt 0 ]; then
|
|
520
|
+
echo "Last Bounce: $(echo "$CONTACT" | jq -r '.last_bounce')"
|
|
521
|
+
fi
|
|
522
|
+
echo ""
|
|
523
|
+
|
|
524
|
+
# Custom attributes
|
|
525
|
+
echo "=== Custom Attributes ==="
|
|
526
|
+
echo "$CONTACT" | jq '.custom_attributes'
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
### Workflow 5: Contact Enrichment
|
|
530
|
+
|
|
531
|
+
```bash
|
|
532
|
+
#!/bin/bash
|
|
533
|
+
# enrich-contact.sh
|
|
534
|
+
|
|
535
|
+
LIST_ID=123
|
|
536
|
+
EMAIL="$1"
|
|
537
|
+
|
|
538
|
+
if [ -z "$EMAIL" ]; then
|
|
539
|
+
echo "Usage: $0 <email>"
|
|
540
|
+
exit 1
|
|
541
|
+
fi
|
|
542
|
+
|
|
543
|
+
echo "=== Contact Enrichment ==="
|
|
544
|
+
echo ""
|
|
545
|
+
|
|
546
|
+
# Find contact
|
|
547
|
+
CONTACTS=$(cakemail contacts list $LIST_ID --filter "email==$EMAIL" -f json)
|
|
548
|
+
CONTACT_ID=$(echo "$CONTACTS" | jq -r '.data[0].id')
|
|
549
|
+
|
|
550
|
+
if [ "$CONTACT_ID" == "null" ]; then
|
|
551
|
+
echo "Contact not found: $EMAIL"
|
|
552
|
+
exit 1
|
|
553
|
+
fi
|
|
554
|
+
|
|
555
|
+
echo "Found contact: $CONTACT_ID"
|
|
556
|
+
echo ""
|
|
557
|
+
|
|
558
|
+
# Get current data
|
|
559
|
+
CONTACT=$(cakemail contacts get $LIST_ID $CONTACT_ID -f json)
|
|
560
|
+
|
|
561
|
+
echo "Current data:"
|
|
562
|
+
echo "$CONTACT" | jq '{email, first_name, last_name, custom_attributes}'
|
|
563
|
+
echo ""
|
|
564
|
+
|
|
565
|
+
# Enrich from external source (example: your CRM)
|
|
566
|
+
echo "Fetching enrichment data..."
|
|
567
|
+
|
|
568
|
+
# Example: Call your API or database
|
|
569
|
+
# ENRICHMENT=$(curl -s "https://api.yourcrm.com/contact?email=$EMAIL")
|
|
570
|
+
|
|
571
|
+
# Mock enrichment data
|
|
572
|
+
ENRICHMENT='{
|
|
573
|
+
"company": "Acme Corp",
|
|
574
|
+
"title": "Marketing Manager",
|
|
575
|
+
"industry": "Technology",
|
|
576
|
+
"company_size": "50-100",
|
|
577
|
+
"linkedin_url": "https://linkedin.com/in/johndoe"
|
|
578
|
+
}'
|
|
579
|
+
|
|
580
|
+
echo "Enrichment data:"
|
|
581
|
+
echo "$ENRICHMENT" | jq '.'
|
|
582
|
+
echo ""
|
|
583
|
+
|
|
584
|
+
read -p "Apply enrichment? (yes/no): " APPLY
|
|
585
|
+
|
|
586
|
+
if [ "$APPLY" == "yes" ]; then
|
|
587
|
+
# Merge with existing custom attributes
|
|
588
|
+
EXISTING=$(echo "$CONTACT" | jq '.custom_attributes')
|
|
589
|
+
MERGED=$(echo "$EXISTING $ENRICHMENT" | jq -s '.[0] + .[1]')
|
|
590
|
+
|
|
591
|
+
# Update contact
|
|
592
|
+
cakemail contacts update $LIST_ID $CONTACT_ID -d "$MERGED"
|
|
593
|
+
|
|
594
|
+
echo ""
|
|
595
|
+
echo "✓ Contact enriched"
|
|
596
|
+
fi
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
## Contact Status Management
|
|
600
|
+
|
|
601
|
+
### Understanding Contact Status
|
|
602
|
+
|
|
603
|
+
**Status values:**
|
|
604
|
+
- `subscribed` - Active, can receive emails
|
|
605
|
+
- `unsubscribed` - Opted out, cannot send
|
|
606
|
+
- `bounced` - Email bounced, delivery failed
|
|
607
|
+
- `pending` - Awaiting confirmation (double opt-in)
|
|
608
|
+
|
|
609
|
+
### Resubscribe Contact
|
|
610
|
+
|
|
611
|
+
```bash
|
|
612
|
+
# Change unsubscribed back to subscribed
|
|
613
|
+
$ cakemail contacts update 123 501 --status subscribed
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
**Note:** Only do this with explicit consent!
|
|
617
|
+
|
|
618
|
+
### Handle Bounced Contacts
|
|
619
|
+
|
|
620
|
+
```bash
|
|
621
|
+
# Find bounced contacts
|
|
622
|
+
$ cakemail contacts list 123 --filter "status==bounced"
|
|
623
|
+
|
|
624
|
+
# If email corrected, mark as subscribed
|
|
625
|
+
$ cakemail contacts update 123 501 --status subscribed
|
|
626
|
+
|
|
627
|
+
# Or delete if permanently invalid
|
|
628
|
+
$ cakemail contacts delete 123 501 --force
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
### Find Pending Confirmations
|
|
632
|
+
|
|
633
|
+
```bash
|
|
634
|
+
# Double opt-in pending
|
|
635
|
+
$ cakemail contacts list 123 --filter "status==pending"
|
|
636
|
+
|
|
637
|
+
# Resend confirmation (if supported)
|
|
638
|
+
# Or manually approve
|
|
639
|
+
$ cakemail contacts update 123 501 --status subscribed
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
## Contact Validation
|
|
643
|
+
|
|
644
|
+
### Validate Email Format
|
|
645
|
+
|
|
646
|
+
```bash
|
|
647
|
+
#!/bin/bash
|
|
648
|
+
# validate-email.sh
|
|
649
|
+
|
|
650
|
+
EMAIL="$1"
|
|
651
|
+
|
|
652
|
+
if [[ ! "$EMAIL" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
|
|
653
|
+
echo "❌ Invalid email format: $EMAIL"
|
|
654
|
+
exit 1
|
|
655
|
+
fi
|
|
656
|
+
|
|
657
|
+
echo "✅ Valid email format: $EMAIL"
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
### Validate Before Adding
|
|
661
|
+
|
|
662
|
+
```bash
|
|
663
|
+
#!/bin/bash
|
|
664
|
+
# safe-add-contact.sh
|
|
665
|
+
|
|
666
|
+
LIST_ID=123
|
|
667
|
+
EMAIL="$1"
|
|
668
|
+
FIRST_NAME="$2"
|
|
669
|
+
LAST_NAME="$3"
|
|
670
|
+
|
|
671
|
+
# Validate email
|
|
672
|
+
if [[ ! "$EMAIL" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
|
|
673
|
+
echo "❌ Invalid email: $EMAIL"
|
|
674
|
+
exit 1
|
|
675
|
+
fi
|
|
676
|
+
|
|
677
|
+
# Check if already exists
|
|
678
|
+
EXISTING=$(cakemail contacts list $LIST_ID --filter "email==$EMAIL" -f json | jq '.count')
|
|
679
|
+
|
|
680
|
+
if [ $EXISTING -gt 0 ]; then
|
|
681
|
+
echo "⚠️ Contact already exists: $EMAIL"
|
|
682
|
+
read -p "Update instead? (yes/no): " UPDATE
|
|
683
|
+
|
|
684
|
+
if [ "$UPDATE" == "yes" ]; then
|
|
685
|
+
CONTACT_ID=$(cakemail contacts list $LIST_ID --filter "email==$EMAIL" -f json | jq -r '.data[0].id')
|
|
686
|
+
cakemail contacts update $LIST_ID $CONTACT_ID -f "$FIRST_NAME" -l "$LAST_NAME"
|
|
687
|
+
echo "✓ Contact updated"
|
|
688
|
+
fi
|
|
689
|
+
exit 0
|
|
690
|
+
fi
|
|
691
|
+
|
|
692
|
+
# Add contact
|
|
693
|
+
cakemail contacts add $LIST_ID -e "$EMAIL" -f "$FIRST_NAME" -l "$LAST_NAME"
|
|
694
|
+
echo "✓ Contact added"
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
## Contact Deduplication
|
|
698
|
+
|
|
699
|
+
### Find Duplicate Emails
|
|
700
|
+
|
|
701
|
+
```bash
|
|
702
|
+
#!/bin/bash
|
|
703
|
+
# find-duplicates.sh
|
|
704
|
+
|
|
705
|
+
LIST_ID=123
|
|
706
|
+
|
|
707
|
+
echo "=== Finding Duplicate Contacts ==="
|
|
708
|
+
echo ""
|
|
709
|
+
|
|
710
|
+
# Export all contacts
|
|
711
|
+
cakemail contacts export $LIST_ID
|
|
712
|
+
EXPORT_ID=$(cakemail contacts export-list -f json | jq -r '.data[0].id')
|
|
713
|
+
|
|
714
|
+
# Wait for export
|
|
715
|
+
sleep 10
|
|
716
|
+
|
|
717
|
+
# Download and analyze
|
|
718
|
+
cakemail contacts export-download $EXPORT_ID > contacts.csv
|
|
719
|
+
|
|
720
|
+
# Find duplicates
|
|
721
|
+
echo "Duplicate emails:"
|
|
722
|
+
cut -d',' -f1 contacts.csv | sort | uniq -d
|
|
723
|
+
|
|
724
|
+
echo ""
|
|
725
|
+
echo "Run manual review to keep best version of each"
|
|
726
|
+
|
|
727
|
+
rm contacts.csv
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
### Remove Duplicates (Keep First)
|
|
731
|
+
|
|
732
|
+
```bash
|
|
733
|
+
#!/bin/bash
|
|
734
|
+
# remove-duplicates.sh
|
|
735
|
+
|
|
736
|
+
LIST_ID=123
|
|
737
|
+
EMAIL="$1"
|
|
738
|
+
|
|
739
|
+
if [ -z "$EMAIL" ]; then
|
|
740
|
+
echo "Usage: $0 <duplicate-email>"
|
|
741
|
+
exit 1
|
|
742
|
+
fi
|
|
743
|
+
|
|
744
|
+
# Find all instances
|
|
745
|
+
CONTACTS=$(cakemail contacts list $LIST_ID --filter "email==$EMAIL" -f json)
|
|
746
|
+
COUNT=$(echo "$CONTACTS" | jq '.count')
|
|
747
|
+
|
|
748
|
+
if [ $COUNT -le 1 ]; then
|
|
749
|
+
echo "No duplicates found for: $EMAIL"
|
|
750
|
+
exit 0
|
|
751
|
+
fi
|
|
752
|
+
|
|
753
|
+
echo "Found $COUNT instances of $EMAIL"
|
|
754
|
+
echo ""
|
|
755
|
+
|
|
756
|
+
# Show all instances
|
|
757
|
+
echo "$CONTACTS" | jq -r '.data[] | "ID: \(.id) - Subscribed: \(.subscribed_on)"'
|
|
758
|
+
echo ""
|
|
759
|
+
|
|
760
|
+
# Keep first, delete rest
|
|
761
|
+
KEEP=$(echo "$CONTACTS" | jq -r '.data[0].id')
|
|
762
|
+
TO_DELETE=$(echo "$CONTACTS" | jq -r '.data[1:][].id')
|
|
763
|
+
|
|
764
|
+
echo "Keeping contact: $KEEP"
|
|
765
|
+
echo "Deleting: $TO_DELETE"
|
|
766
|
+
echo ""
|
|
767
|
+
|
|
768
|
+
read -p "Proceed? (yes/no): " CONFIRM
|
|
769
|
+
|
|
770
|
+
if [ "$CONFIRM" == "yes" ]; then
|
|
771
|
+
for ID in $TO_DELETE; do
|
|
772
|
+
cakemail contacts delete $LIST_ID $ID --force
|
|
773
|
+
echo " Deleted: $ID"
|
|
774
|
+
done
|
|
775
|
+
echo "✓ Deduplication complete"
|
|
776
|
+
fi
|
|
777
|
+
```
|
|
778
|
+
|
|
779
|
+
## Contact Segmentation
|
|
780
|
+
|
|
781
|
+
### Create Targeted Lists from Filters
|
|
782
|
+
|
|
783
|
+
```bash
|
|
784
|
+
#!/bin/bash
|
|
785
|
+
# extract-segment.sh
|
|
786
|
+
|
|
787
|
+
SOURCE_LIST=123
|
|
788
|
+
TARGET_LIST=124
|
|
789
|
+
FILTER="custom_attributes.plan==premium"
|
|
790
|
+
|
|
791
|
+
echo "=== Extracting Segment ==="
|
|
792
|
+
echo "Source: $SOURCE_LIST"
|
|
793
|
+
echo "Target: $TARGET_LIST"
|
|
794
|
+
echo "Filter: $FILTER"
|
|
795
|
+
echo ""
|
|
796
|
+
|
|
797
|
+
# Get filtered contacts
|
|
798
|
+
CONTACTS=$(cakemail contacts list $SOURCE_LIST --filter "$FILTER" -f json | jq -r '.data[].email')
|
|
799
|
+
|
|
800
|
+
COUNT=$(echo "$CONTACTS" | wc -l)
|
|
801
|
+
echo "Found $COUNT contacts matching filter"
|
|
802
|
+
echo ""
|
|
803
|
+
|
|
804
|
+
read -p "Copy to target list? (yes/no): " CONFIRM
|
|
805
|
+
|
|
806
|
+
if [ "$CONFIRM" == "yes" ]; then
|
|
807
|
+
for EMAIL in $CONTACTS; do
|
|
808
|
+
# Get full contact data
|
|
809
|
+
CONTACT=$(cakemail contacts list $SOURCE_LIST --filter "email==$EMAIL" -f json | jq '.data[0]')
|
|
810
|
+
|
|
811
|
+
FIRST_NAME=$(echo "$CONTACT" | jq -r '.first_name')
|
|
812
|
+
LAST_NAME=$(echo "$CONTACT" | jq -r '.last_name')
|
|
813
|
+
CUSTOM_ATTRS=$(echo "$CONTACT" | jq -c '.custom_attributes')
|
|
814
|
+
|
|
815
|
+
# Add to target list
|
|
816
|
+
cakemail contacts add $TARGET_LIST \
|
|
817
|
+
-e "$EMAIL" \
|
|
818
|
+
-f "$FIRST_NAME" \
|
|
819
|
+
-l "$LAST_NAME" \
|
|
820
|
+
-d "$CUSTOM_ATTRS" 2>/dev/null
|
|
821
|
+
|
|
822
|
+
echo " Copied: $EMAIL"
|
|
823
|
+
done
|
|
824
|
+
|
|
825
|
+
echo ""
|
|
826
|
+
echo "✓ Segment extracted"
|
|
827
|
+
fi
|
|
828
|
+
```
|
|
829
|
+
|
|
830
|
+
## Contact Analytics
|
|
831
|
+
|
|
832
|
+
### Engagement Scoring
|
|
833
|
+
|
|
834
|
+
```bash
|
|
835
|
+
#!/bin/bash
|
|
836
|
+
# engagement-score.sh
|
|
837
|
+
|
|
838
|
+
LIST_ID=123
|
|
839
|
+
CONTACT_ID=$1
|
|
840
|
+
|
|
841
|
+
if [ -z "$CONTACT_ID" ]; then
|
|
842
|
+
echo "Usage: $0 <contact-id>"
|
|
843
|
+
exit 1
|
|
844
|
+
fi
|
|
845
|
+
|
|
846
|
+
CONTACT=$(cakemail contacts get $LIST_ID $CONTACT_ID -f json)
|
|
847
|
+
|
|
848
|
+
# Get metrics
|
|
849
|
+
TOTAL_OPENS=$(echo "$CONTACT" | jq -r '.total_opens // 0')
|
|
850
|
+
TOTAL_CLICKS=$(echo "$CONTACT" | jq -r '.total_clicks // 0')
|
|
851
|
+
LAST_OPEN=$(echo "$CONTACT" | jq -r '.last_open_date // "null"')
|
|
852
|
+
|
|
853
|
+
# Calculate score (0-100)
|
|
854
|
+
SCORE=0
|
|
855
|
+
|
|
856
|
+
# Opens contribute 30 points max
|
|
857
|
+
if [ $TOTAL_OPENS -gt 0 ]; then
|
|
858
|
+
OPEN_SCORE=$((TOTAL_OPENS > 30 ? 30 : TOTAL_OPENS))
|
|
859
|
+
SCORE=$((SCORE + OPEN_SCORE))
|
|
860
|
+
fi
|
|
861
|
+
|
|
862
|
+
# Clicks contribute 40 points max
|
|
863
|
+
if [ $TOTAL_CLICKS -gt 0 ]; then
|
|
864
|
+
CLICK_SCORE=$((TOTAL_CLICKS * 4))
|
|
865
|
+
CLICK_SCORE=$((CLICK_SCORE > 40 ? 40 : CLICK_SCORE))
|
|
866
|
+
SCORE=$((SCORE + CLICK_SCORE))
|
|
867
|
+
fi
|
|
868
|
+
|
|
869
|
+
# Recent activity contributes 30 points
|
|
870
|
+
if [ "$LAST_OPEN" != "null" ]; then
|
|
871
|
+
DAYS_AGO=$(( ($(date +%s) - $(date -d "$LAST_OPEN" +%s)) / 86400 ))
|
|
872
|
+
|
|
873
|
+
if [ $DAYS_AGO -lt 7 ]; then
|
|
874
|
+
SCORE=$((SCORE + 30))
|
|
875
|
+
elif [ $DAYS_AGO -lt 30 ]; then
|
|
876
|
+
SCORE=$((SCORE + 20))
|
|
877
|
+
elif [ $DAYS_AGO -lt 90 ]; then
|
|
878
|
+
SCORE=$((SCORE + 10))
|
|
879
|
+
fi
|
|
880
|
+
fi
|
|
881
|
+
|
|
882
|
+
echo "=== Engagement Score ==="
|
|
883
|
+
echo "Contact: $(echo "$CONTACT" | jq -r '.email')"
|
|
884
|
+
echo "Total Opens: $TOTAL_OPENS"
|
|
885
|
+
echo "Total Clicks: $TOTAL_CLICKS"
|
|
886
|
+
echo "Last Open: ${LAST_OPEN:-Never}"
|
|
887
|
+
echo ""
|
|
888
|
+
echo "Engagement Score: $SCORE/100"
|
|
889
|
+
echo ""
|
|
890
|
+
|
|
891
|
+
if [ $SCORE -ge 70 ]; then
|
|
892
|
+
echo "🟢 Highly Engaged"
|
|
893
|
+
elif [ $SCORE -ge 40 ]; then
|
|
894
|
+
echo "🟡 Moderately Engaged"
|
|
895
|
+
elif [ $SCORE -ge 20 ]; then
|
|
896
|
+
echo "🟠 Low Engagement"
|
|
897
|
+
else
|
|
898
|
+
echo "🔴 At Risk - Very Low Engagement"
|
|
899
|
+
fi
|
|
900
|
+
```
|
|
901
|
+
|
|
902
|
+
### Contact Lifetime Value
|
|
903
|
+
|
|
904
|
+
```bash
|
|
905
|
+
#!/bin/bash
|
|
906
|
+
# calculate-ltv.sh
|
|
907
|
+
|
|
908
|
+
LIST_ID=123
|
|
909
|
+
|
|
910
|
+
echo "=== Contact Lifetime Value Analysis ==="
|
|
911
|
+
echo ""
|
|
912
|
+
|
|
913
|
+
# Get all contacts with LTV data
|
|
914
|
+
CONTACTS=$(cakemail contacts list $LIST_ID \
|
|
915
|
+
--filter "status==subscribed" \
|
|
916
|
+
-f json | jq -r '.data[] | "\(.id),\(.email),\(.custom_attributes.lifetime_value // 0)"')
|
|
917
|
+
|
|
918
|
+
TOTAL_LTV=0
|
|
919
|
+
COUNT=0
|
|
920
|
+
|
|
921
|
+
echo "Contact ID | Email | LTV"
|
|
922
|
+
echo "-----------|-------|-----"
|
|
923
|
+
|
|
924
|
+
while IFS=',' read ID EMAIL LTV; do
|
|
925
|
+
printf "%-10s | %-25s | $%7.2f\n" "$ID" "$EMAIL" "$LTV"
|
|
926
|
+
TOTAL_LTV=$(echo "$TOTAL_LTV + $LTV" | bc)
|
|
927
|
+
COUNT=$((COUNT + 1))
|
|
928
|
+
done <<< "$CONTACTS"
|
|
929
|
+
|
|
930
|
+
echo ""
|
|
931
|
+
echo "Total Contacts: $COUNT"
|
|
932
|
+
echo "Total LTV: \$$(echo "scale=2; $TOTAL_LTV" | bc)"
|
|
933
|
+
echo "Average LTV: \$$(echo "scale=2; $TOTAL_LTV / $COUNT" | bc)"
|
|
934
|
+
```
|
|
935
|
+
|
|
936
|
+
## Troubleshooting
|
|
937
|
+
|
|
938
|
+
### Cannot Add Contact - Email Already Exists
|
|
939
|
+
|
|
940
|
+
**Error:** "Contact with this email already exists"
|
|
941
|
+
|
|
942
|
+
**Solution:**
|
|
943
|
+
```bash
|
|
944
|
+
# Find existing contact
|
|
945
|
+
$ cakemail contacts list 123 --filter "email==john@example.com" -f json
|
|
946
|
+
|
|
947
|
+
# Update instead of add
|
|
948
|
+
CONTACT_ID=$(cakemail contacts list 123 --filter "email==john@example.com" -f json | jq -r '.data[0].id')
|
|
949
|
+
$ cakemail contacts update 123 $CONTACT_ID -f "John" -l "Doe"
|
|
950
|
+
```
|
|
951
|
+
|
|
952
|
+
### Contact Not Found
|
|
953
|
+
|
|
954
|
+
**Error:** "Contact ID not found"
|
|
955
|
+
|
|
956
|
+
**Solution:**
|
|
957
|
+
```bash
|
|
958
|
+
# Verify contact ID
|
|
959
|
+
$ cakemail contacts list 123 --filter "email==john@example.com"
|
|
960
|
+
|
|
961
|
+
# Ensure correct list ID
|
|
962
|
+
$ cakemail lists list
|
|
963
|
+
|
|
964
|
+
# Contact may have been deleted
|
|
965
|
+
# Search across all lists if needed
|
|
966
|
+
```
|
|
967
|
+
|
|
968
|
+
### Custom Attribute Not Saving
|
|
969
|
+
|
|
970
|
+
**Problem:** Custom attributes not appearing
|
|
971
|
+
|
|
972
|
+
**Solution:**
|
|
973
|
+
```bash
|
|
974
|
+
# Ensure attribute exists
|
|
975
|
+
$ cakemail attributes list 123
|
|
976
|
+
|
|
977
|
+
# Create attribute first
|
|
978
|
+
$ cakemail attributes create 123 -n "plan" -t "text"
|
|
979
|
+
|
|
980
|
+
# Then add contact with attribute
|
|
981
|
+
$ cakemail contacts add 123 -e "user@example.com" -d '{"plan":"premium"}'
|
|
982
|
+
|
|
983
|
+
# Verify
|
|
984
|
+
$ cakemail contacts get 123 <contact-id> -f json | jq '.custom_attributes'
|
|
985
|
+
```
|
|
986
|
+
|
|
987
|
+
### Invalid Data Format
|
|
988
|
+
|
|
989
|
+
**Error:** "Invalid JSON format for custom attributes"
|
|
990
|
+
|
|
991
|
+
**Solution:**
|
|
992
|
+
```bash
|
|
993
|
+
# ❌ Wrong: Single quotes for JSON
|
|
994
|
+
$ cakemail contacts add 123 -e "user@example.com" -d '{plan:premium}'
|
|
995
|
+
|
|
996
|
+
# ✅ Correct: Proper JSON with double quotes
|
|
997
|
+
$ cakemail contacts add 123 -e "user@example.com" -d '{"plan":"premium"}'
|
|
998
|
+
|
|
999
|
+
# ✅ Correct: Escaped in shell
|
|
1000
|
+
$ cakemail contacts add 123 -e "user@example.com" -d "{\"plan\":\"premium\"}"
|
|
1001
|
+
```
|
|
1002
|
+
|
|
1003
|
+
### Bulk Operations Too Slow
|
|
1004
|
+
|
|
1005
|
+
**Problem:** Adding many contacts individually is slow
|
|
1006
|
+
|
|
1007
|
+
**Solution:**
|
|
1008
|
+
```bash
|
|
1009
|
+
# Use import instead of individual adds
|
|
1010
|
+
# See contact-import-export.md guide
|
|
1011
|
+
|
|
1012
|
+
# Create CSV file
|
|
1013
|
+
echo "email,first_name,last_name,plan" > contacts.csv
|
|
1014
|
+
echo "user1@example.com,John,Doe,premium" >> contacts.csv
|
|
1015
|
+
echo "user2@example.com,Jane,Smith,basic" >> contacts.csv
|
|
1016
|
+
|
|
1017
|
+
# Import in bulk
|
|
1018
|
+
$ cakemail contacts import 123 --file contacts.csv
|
|
1019
|
+
|
|
1020
|
+
# Much faster than individual adds
|
|
1021
|
+
```
|
|
1022
|
+
|
|
1023
|
+
## Best Practices Summary
|
|
1024
|
+
|
|
1025
|
+
1. **Validate before adding** - Check email format and duplicates
|
|
1026
|
+
2. **Use custom attributes** - Store business-specific data
|
|
1027
|
+
3. **Keep data current** - Update contacts regularly
|
|
1028
|
+
4. **Track consent** - Document subscription source and date
|
|
1029
|
+
5. **Monitor engagement** - Identify and re-engage inactive contacts
|
|
1030
|
+
6. **Clean regularly** - Remove bounced and invalid contacts
|
|
1031
|
+
7. **Bulk operations** - Use import for adding many contacts
|
|
1032
|
+
8. **Segment intelligently** - Use attributes for targeting
|
|
1033
|
+
9. **Document schema** - Keep track of custom attribute meanings
|
|
1034
|
+
10. **Respect unsubscribes** - Never resubscribe without explicit consent
|
|
1035
|
+
|