feedbackbasket-cli 0.8.0 → 0.9.1

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/README.md CHANGED
@@ -93,7 +93,9 @@ feedbackbasket feedback create "Title" --content "Body" --project myapp
93
93
  feedbackbasket feedback create "Login bug" --content "Clicking Log in does nothing" --project myapp --type bug --page-url https://example.com/login
94
94
  feedbackbasket feedback update <id> --status PLANNED # Update status
95
95
  feedbackbasket feedback update <id> --category BUG # Update category
96
- feedbackbasket feedback reply <id> "Thanks for reporting!" # Email the submitter
96
+ feedbackbasket feedback reply <id> "Thanks!" --delivery email --reply-to support@example.com
97
+ feedbackbasket feedback reply <id> "Thanks!" --delivery widget
98
+ feedbackbasket feedback reply <id> "Thanks!" --delivery both --reply-to support@example.com
97
99
  feedbackbasket feedback replies <id> # List sent replies
98
100
  feedbackbasket feedback note <id> "Investigating this..." # Add internal note
99
101
  feedbackbasket feedback delete <id> # Delete feedback
@@ -139,6 +141,16 @@ feedbackbasket widget flow myapp --config ./feedback-flow.json
139
141
  feedbackbasket widget script myapp
140
142
  ```
141
143
 
144
+ For inline trigger mode, load the widget once and call the public API from your own button:
145
+
146
+ ```html
147
+ <button onclick="window.FeedbackWidget.openFeedbackForm({ trigger: event.currentTarget })">
148
+ Feedback
149
+ </button>
150
+ ```
151
+
152
+ Passing the trigger element lets popup mode open beside your custom button. Calling `window.FeedbackWidget.openFeedbackForm()` with no arguments still uses the configured widget position.
153
+
142
154
  Use `--email-read-only` and `--hide-email-when-prefilled` with runtime `userEmail` values from your app. These settings do not store visitor emails in FeedbackBasket widget settings.
143
155
 
144
156
  `widget flow --config` accepts either a `feedbackFlow` object or a JSON object with a `feedbackFlow` key. V1 supports guided mode with `text`, `textarea`, and `single_choice` follow-up questions.
@@ -1,4 +1,4 @@
1
- import type { ProjectsResponse, FeedbackResponse, BugReportsResponse, FeedbackParams, FeedbackCreateInput, FeedbackCreateResponse, BugReportParams, UserProfile, Project, Feedback, WidgetSettings } from './types.js';
1
+ import type { ProjectsResponse, FeedbackResponse, BugReportsResponse, FeedbackParams, FeedbackCreateInput, FeedbackCreateResponse, FeedbackReplyResponse, BugReportParams, UserProfile, Project, Feedback, WidgetSettings } from './types.js';
2
2
  export declare class FeedbackBasketClient {
3
3
  private http;
4
4
  constructor(token: string, baseUrl: string);
@@ -61,16 +61,10 @@ export declare class FeedbackBasketClient {
61
61
  content: string;
62
62
  createdAt: string;
63
63
  }>;
64
- sendReply(feedbackId: string, content: string, replyToEmail?: string): Promise<{
65
- reply: {
66
- id: string;
67
- content: string;
68
- replyToEmail: string;
69
- sentBy: string;
70
- createdAt: string;
71
- };
72
- sentTo: string;
73
- }>;
64
+ sendReply(feedbackId: string, content: string, opts?: {
65
+ replyToEmail?: string;
66
+ destinations?: Array<'email' | 'widget'>;
67
+ }): Promise<FeedbackReplyResponse>;
74
68
  listReplies(feedbackId: string): Promise<{
75
69
  replies: Array<{
76
70
  id: string;
@@ -79,6 +73,7 @@ export declare class FeedbackBasketClient {
79
73
  sentBy: string;
80
74
  createdAt: string;
81
75
  }>;
76
+ messages?: NonNullable<FeedbackReplyResponse['message']>[];
82
77
  total: number;
83
78
  }>;
84
79
  deleteFeedback(id: string): Promise<{
@@ -70,8 +70,11 @@ export class FeedbackBasketClient {
70
70
  async createNote(feedbackId, content) {
71
71
  return this.request('POST', `/feedback/${encodeURIComponent(feedbackId)}/notes`, { content });
72
72
  }
73
- async sendReply(feedbackId, content, replyToEmail) {
73
+ async sendReply(feedbackId, content, opts = {}) {
74
74
  const body = { content };
75
+ if (opts.destinations)
76
+ body.destinations = opts.destinations;
77
+ const replyToEmail = opts.replyToEmail;
75
78
  if (replyToEmail)
76
79
  body['replyToEmail'] = replyToEmail;
77
80
  return this.request('POST', `/feedback/${encodeURIComponent(feedbackId)}/replies`, body);
@@ -4,79 +4,84 @@ import { AuthManager } from '../auth/manager.js';
4
4
  import { loadConfig } from '../config/config.js';
5
5
  import { errAuth, errUsage } from '../output/errors.js';
6
6
  import { brand } from '../output/theme.js';
7
- import { ask, confirm } from '../prompt.js';
7
+ import { ask } from '../prompt.js';
8
+ const deliveryOptions = new Set(['email', 'widget', 'both']);
8
9
  export function createFeedbackReplyCommand(getWriter) {
9
10
  return new Command('reply')
10
11
  .argument('<id>', 'Feedback ID to reply to')
11
12
  .argument('[content]', 'Reply content (or use --content)')
12
- .description('Send an email reply to the feedback submitter')
13
+ .description('Reply to feedback by email, widget thread, or both')
13
14
  .option('--content <text>', 'Reply content (alternative to positional argument)')
14
- .option('--reply-to <email>', 'Reply-to email (overrides project default)')
15
+ .option('--delivery <delivery>', 'Reply delivery (email, widget, both)', 'email')
16
+ .option('--reply-to <email>', 'Reply-to email for email delivery')
15
17
  .action(async (id, contentArg, opts) => {
16
18
  const writer = getWriter();
17
19
  const content = contentArg ?? opts.content;
20
+ const delivery = opts.delivery;
18
21
  if (!content) {
19
- throw errUsage('Reply content is required', 'Example: feedbackbasket feedback reply <id> "Thanks for reporting this!"');
22
+ throw errUsage('Reply content is required', 'Example: feedbackbasket feedback reply <id> "Thanks for reporting this!" --delivery widget');
23
+ }
24
+ if (!deliveryOptions.has(delivery)) {
25
+ throw errUsage('Delivery must be email, widget, or both', 'Example: feedbackbasket feedback reply <id> "Thanks!" --delivery both --reply-to support@example.com');
20
26
  }
21
27
  const client = requireClient();
22
- let replyTo = opts.replyTo;
23
- // If no --reply-to flag, check if we need to resolve one interactively
24
- if (!replyTo) {
25
- const feedback = await client.getFeedbackById(id);
28
+ const feedback = await client.getFeedbackById(id);
29
+ const destinations = delivery === 'both'
30
+ ? ['email', 'widget']
31
+ : [delivery];
32
+ const sendsEmail = destinations.includes('email');
33
+ const sendsWidget = destinations.includes('widget');
34
+ let replyTo = opts.replyTo ?? feedback.project.replyToEmail ?? undefined;
35
+ if (sendsEmail) {
26
36
  if (!feedback.email) {
27
- throw errUsage('This feedback has no email address cannot send a reply.');
37
+ throw errUsage('This feedback has no email address; use --delivery widget if it has a widget thread.');
28
38
  }
29
- const projectReplyTo = feedback.project.replyToEmail;
30
- if (!projectReplyTo) {
39
+ if (!replyTo) {
31
40
  const isInteractive = !writer.isMachineOutput() && process.stdin.isTTY;
32
- if (isInteractive) {
33
- // Get user's email as suggestion
34
- const manager = new AuthManager();
35
- const creds = manager.getCredentials();
36
- const accountEmail = creds?.email;
37
- console.log();
38
- console.log(brand.warning(' No reply-to email configured for this project.'));
39
- console.log(brand.muted(' The recipient will see this as the sender address.'));
40
- console.log();
41
- if (accountEmail) {
42
- const useAccount = await confirm(` Use ${brand.bold(accountEmail)}?`);
43
- if (useAccount) {
44
- replyTo = accountEmail;
45
- }
46
- else {
47
- replyTo = await ask(' Enter reply-to email: ');
48
- if (!replyTo || !replyTo.includes('@')) {
49
- console.log(brand.muted(' Cancelled.'));
50
- return;
51
- }
52
- }
53
- }
54
- else {
55
- replyTo = await ask(' Enter reply-to email: ');
56
- if (!replyTo || !replyTo.includes('@')) {
57
- console.log(brand.muted(' Cancelled.'));
58
- return;
59
- }
60
- }
61
- console.log();
62
- console.log(brand.muted(` Tip: Set a default with: feedbackbasket projects update ${feedback.project.name} --reply-to ${replyTo}`));
63
- console.log();
41
+ if (!isInteractive) {
42
+ throw errUsage('No reply-to email configured for this project.', 'Ask the human which reply-to email to use, then pass --reply-to <email> or set a project default.');
64
43
  }
65
- else {
66
- // Agent mode — must pass --reply-to or set project default
67
- throw errUsage('No reply-to email configured for this project.', `Pass --reply-to <email> or set a default: feedbackbasket projects update <project> --reply-to <email>`);
44
+ console.log();
45
+ console.log(brand.warning(' No reply-to email configured for this project.'));
46
+ console.log(brand.muted(' The recipient will see this as the sender address.'));
47
+ console.log();
48
+ replyTo = await ask(' Enter reply-to email: ');
49
+ if (!replyTo || !replyTo.includes('@')) {
50
+ console.log(brand.muted(' Cancelled.'));
51
+ return;
68
52
  }
53
+ console.log();
54
+ console.log(brand.muted(` Tip: Set a default with: feedbackbasket projects update ${feedback.project.name} --reply-to ${replyTo}`));
55
+ console.log();
69
56
  }
70
57
  }
71
- const result = await client.sendReply(id, content, replyTo);
58
+ if (sendsWidget && !feedback.hasWidgetAccess) {
59
+ throw errUsage('This feedback is not connected to a widget thread.', 'Use --delivery email for feedback with an email address, or ask the human how they want to respond.');
60
+ }
61
+ const result = await client.sendReply(id, content, {
62
+ replyToEmail: replyTo,
63
+ destinations,
64
+ });
72
65
  if (!writer.isMachineOutput()) {
73
- console.log(` ${brand.success('✓')} Reply sent to ${brand.bold(result.sentTo)}`);
74
- console.log(` ${brand.muted('From:')} ${result.reply.replyToEmail}`);
75
- console.log(` ${brand.muted('By:')} ${result.reply.sentBy}`);
66
+ if (result.sentTo) {
67
+ console.log(` ${brand.success('[OK]')} Email reply sent to ${brand.bold(result.sentTo)}`);
68
+ if (result.reply) {
69
+ console.log(` ${brand.muted('From:')} ${result.reply.replyToEmail}`);
70
+ console.log(` ${brand.muted('By:')} ${result.reply.sentBy}`);
71
+ }
72
+ }
73
+ if (result.message) {
74
+ console.log(` ${brand.success('[OK]')} Widget reply posted`);
75
+ console.log(` ${brand.muted('By:')} ${result.message.sentByName ?? 'CLI'}`);
76
+ }
76
77
  console.log();
77
78
  }
78
79
  writer.ok(result, {
79
- summary: `Reply sent to ${result.sentTo}`,
80
+ summary: delivery === 'both'
81
+ ? 'Reply sent by email and widget'
82
+ : delivery === 'widget'
83
+ ? 'Widget reply posted'
84
+ : `Reply sent to ${result.sentTo}`,
80
85
  breadcrumbs: [
81
86
  { action: 'View replies', cmd: `feedbackbasket feedback replies ${id}` },
82
87
  { action: 'Update status', cmd: `feedbackbasket feedback update ${id} --status COMPLETE` },
@@ -93,8 +98,10 @@ export function createFeedbackRepliesCommand(getWriter) {
93
98
  const writer = getWriter();
94
99
  const client = requireClient();
95
100
  const result = await client.listReplies(id);
101
+ const messages = result.messages ?? [];
102
+ const visibleWidgetMessages = messages.filter((item) => !item.replyId);
96
103
  if (!writer.isMachineOutput()) {
97
- if (result.replies.length === 0) {
104
+ if (result.total === 0) {
98
105
  console.log(brand.muted(' No replies sent yet.'));
99
106
  console.log();
100
107
  }
@@ -102,7 +109,8 @@ export function createFeedbackRepliesCommand(getWriter) {
102
109
  console.log(brand.bold(`${result.total} repl${result.total === 1 ? 'y' : 'ies'} for feedback ${id}`));
103
110
  console.log();
104
111
  for (const r of result.replies) {
105
- console.log(` ${brand.success('')} ${brand.bold(r.sentBy)} ${brand.muted(r.createdAt)}`);
112
+ console.log(` ${brand.success('->')} ${brand.bold(r.sentBy)} ${brand.muted(r.createdAt)}`);
113
+ console.log(` ${brand.muted('Delivery:')} email`);
106
114
  console.log(` ${brand.muted('Reply-to:')} ${r.replyToEmail}`);
107
115
  console.log();
108
116
  for (const line of r.content.split('\n')) {
@@ -110,12 +118,22 @@ export function createFeedbackRepliesCommand(getWriter) {
110
118
  }
111
119
  console.log();
112
120
  }
121
+ for (const message of visibleWidgetMessages) {
122
+ console.log(` ${brand.success('->')} ${brand.bold(message.sentByName ?? 'CLI')} ${brand.muted(message.createdAt)}`);
123
+ console.log(` ${brand.muted('Delivery:')} widget`);
124
+ console.log();
125
+ for (const line of message.content.split('\n')) {
126
+ console.log(` ${line}`);
127
+ }
128
+ console.log();
129
+ }
113
130
  }
114
131
  }
115
- writer.ok(result.replies, {
132
+ writer.ok({ replies: result.replies, messages }, {
116
133
  summary: `${result.total} repl${result.total === 1 ? 'y' : 'ies'}`,
117
134
  breadcrumbs: [
118
- { action: 'Send a reply', cmd: `feedbackbasket feedback reply ${id} "<content>"` },
135
+ { action: 'Send an email reply', cmd: `feedbackbasket feedback reply ${id} "<content>" --delivery email` },
136
+ { action: 'Post a widget reply', cmd: `feedbackbasket feedback reply ${id} "<content>" --delivery widget` },
119
137
  { action: 'View feedback', cmd: `feedbackbasket feedback show ${id}` },
120
138
  ],
121
139
  });
@@ -186,6 +186,7 @@ function renderFeedbackDetail(item) {
186
186
  ['OS', item.os],
187
187
  ['Device', item.device],
188
188
  ['Language', item.language],
189
+ ['Widget Thread', item.hasWidgetAccess ? 'Yes' : null],
189
190
  ['Created', item.createdAt],
190
191
  ];
191
192
  for (const [label, value] of fields) {
@@ -243,6 +243,7 @@ export function createWidgetCommand(getWriter) {
243
243
  const projectId = await resolveProjectId(client, projectArg);
244
244
  const result = await client.getWidgetScript(projectId);
245
245
  if (!writer.isMachineOutput()) {
246
+ const settingsResult = await client.getWidgetSettings(projectId);
246
247
  console.log(brand.bold(`Widget embed code for ${result.projectName}`));
247
248
  console.log(divider(50));
248
249
  console.log();
@@ -252,6 +253,7 @@ export function createWidgetCommand(getWriter) {
252
253
  console.log();
253
254
  console.log(brand.muted(` Script URL: ${result.scriptUrl}`));
254
255
  console.log();
256
+ renderInlineTriggerHelp(settingsResult.settings);
255
257
  }
256
258
  writer.ok(result, {
257
259
  summary: `Embed code for "${result.projectName}"`,
@@ -341,6 +343,27 @@ function isRecord(value) {
341
343
  function stringValue(value) {
342
344
  return typeof value === 'string' ? value.trim() : '';
343
345
  }
346
+ function renderInlineTriggerHelp(settings) {
347
+ if (settings.triggerMode !== 'inline')
348
+ return;
349
+ console.log(brand.bold('Custom inline trigger'));
350
+ console.log(divider(50));
351
+ console.log();
352
+ console.log(brand.muted(' Call this from your own button:'));
353
+ console.log();
354
+ console.log(` ${brand.primary('<button onclick="window.FeedbackWidget.openFeedbackForm({ trigger: event.currentTarget })">')}`);
355
+ console.log(` ${brand.primary(' Feedback')}`);
356
+ console.log(` ${brand.primary('</button>')}`);
357
+ console.log();
358
+ if (settings.displayMode === 'popup') {
359
+ console.log(brand.muted(' Passing the trigger keeps popup mode anchored beside your custom button.'));
360
+ }
361
+ else {
362
+ console.log(brand.muted(' Modal mode stays centered; passing the trigger is safe for future popup changes.'));
363
+ }
364
+ console.log(brand.muted(' Existing calls to window.FeedbackWidget.openFeedbackForm() still work.'));
365
+ console.log();
366
+ }
344
367
  function renderWidgetSettings(projectName, settings) {
345
368
  console.log(brand.bold(`Widget settings — ${projectName}`));
346
369
  console.log(divider(40));
@@ -2,6 +2,7 @@ export type FeedbackStatus = 'OPEN' | 'UNDER_REVIEW' | 'PLANNED' | 'IN_PROGRESS'
2
2
  export type FeedbackCategory = 'BUG' | 'FEATURE_REQUEST' | 'IMPROVEMENT' | 'QUESTION';
3
3
  export type Sentiment = 'POSITIVE' | 'NEGATIVE' | 'NEUTRAL';
4
4
  export type Severity = 'high' | 'medium' | 'low';
5
+ export type ReplyDelivery = 'email' | 'widget' | 'both';
5
6
  export type FeedbackFlowQuestionType = 'text' | 'textarea' | 'single_choice';
6
7
  export interface FeedbackFlowQuestion {
7
8
  id: string;
@@ -84,6 +85,7 @@ export interface Feedback {
84
85
  os?: string | null;
85
86
  device?: string | null;
86
87
  language?: string | null;
88
+ hasWidgetAccess?: boolean;
87
89
  attachments?: FeedbackAttachment[];
88
90
  project: {
89
91
  id: string;
@@ -141,6 +143,25 @@ export interface FeedbackCreateResponse {
141
143
  url: string;
142
144
  feedback: Feedback;
143
145
  }
146
+ export interface FeedbackReplyResponse {
147
+ reply: {
148
+ id: string;
149
+ content: string;
150
+ replyToEmail: string;
151
+ sentBy: string;
152
+ createdAt: string;
153
+ } | null;
154
+ message: {
155
+ id: string;
156
+ content: string;
157
+ delivery: 'WIDGET' | 'EMAIL' | 'BOTH';
158
+ sentByName: string | null;
159
+ replyId: string | null;
160
+ createdAt: string;
161
+ } | null;
162
+ sentTo: string | null;
163
+ destinations: Array<'email' | 'widget'>;
164
+ }
144
165
  export interface BugReportsResponse {
145
166
  bugReports: BugReport[];
146
167
  stats: {
@@ -1,2 +1,2 @@
1
- export declare const VERSION = "0.8.0";
2
- export declare const USER_AGENT = "FeedbackBasket-CLI/0.8.0";
1
+ export declare const VERSION = "0.9.1";
2
+ export declare const USER_AGENT = "FeedbackBasket-CLI/0.9.1";
@@ -1,2 +1,2 @@
1
- export const VERSION = '0.8.0';
1
+ export const VERSION = '0.9.1';
2
2
  export const USER_AGENT = `FeedbackBasket-CLI/${VERSION}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "feedbackbasket-cli",
3
- "version": "0.8.0",
3
+ "version": "0.9.1",
4
4
  "description": "Command-line interface for FeedbackBasket — manage feedback from your terminal",
5
5
  "type": "module",
6
6
  "main": "dist/src/cli.js",
@@ -69,9 +69,10 @@ feedbackbasket feedback note <id> "Investigating — appears related to auth flo
69
69
  feedbackbasket feedback delete <id> --yes
70
70
  feedbackbasket feedback bulk-update --status CLOSED --ids id1,id2,id3
71
71
 
72
- # Reply to submitter via email
73
- feedbackbasket feedback reply <id> "Thanks for reporting — we pushed a fix!"
74
- feedbackbasket feedback reply <id> "<content>" --reply-to vlad@example.com # override reply-to
72
+ # Reply to submitter by email, widget thread, or both
73
+ feedbackbasket feedback reply <id> "Thanks for reporting — we pushed a fix!" --delivery email --reply-to support@example.com
74
+ feedbackbasket feedback reply <id> "<content>" --delivery widget
75
+ feedbackbasket feedback reply <id> "<content>" --delivery both --reply-to support@example.com
75
76
  feedbackbasket feedback replies <id> # list past replies
76
77
 
77
78
  # Export
@@ -108,6 +109,24 @@ feedbackbasket widget flow <project> --reset-default --enable
108
109
  feedbackbasket widget flow <project> --config ./feedback-flow.json
109
110
  ```
110
111
 
112
+ For inline trigger mode, load the widget once and call the public API from the host app's custom button:
113
+
114
+ ```html
115
+ <button onclick="window.FeedbackWidget.openFeedbackForm({ trigger: event.currentTarget })">
116
+ Feedback
117
+ </button>
118
+ ```
119
+
120
+ In React:
121
+
122
+ ```tsx
123
+ <button onClick={(event) => window.FeedbackWidget.openFeedbackForm({ trigger: event.currentTarget })}>
124
+ Feedback
125
+ </button>
126
+ ```
127
+
128
+ Passing the trigger element lets popup mode open beside the custom button. Calling `window.FeedbackWidget.openFeedbackForm()` with no arguments still uses the configured widget position.
129
+
111
130
  `email-read-only` and `hide-email-when-prefilled` control behavior only when the host app passes a runtime `userEmail` value. Do not store visitor emails in widget settings.
112
131
 
113
132
  `widget flow --config` accepts either a `feedbackFlow` object or a JSON object with a `feedbackFlow` key. Use it when an agent needs to customize visitor choices such as Bug report, Feature request, and General feedback. Supported v1 question types are `text`, `textarea`, and `single_choice`.
@@ -166,18 +185,22 @@ feedbackbasket feedback show <id> --agent
166
185
 
167
186
  ### Close the loop — reply to the submitter
168
187
  ```bash
169
- # Agent reads context, drafts its own reply, sends it
170
- feedbackbasket feedback show <id> --agent # read context + project.replyToEmail
171
- feedbackbasket feedback reply <id> "<drafted response>" --agent
188
+ # Agent reads context, asks which delivery method to use, then sends it
189
+ feedbackbasket feedback show <id> --agent # read email, hasWidgetAccess, project.replyToEmail
190
+ feedbackbasket feedback reply <id> "<drafted response>" --delivery widget --agent
191
+ feedbackbasket feedback reply <id> "<drafted response>" --delivery email --reply-to support@example.com --agent
192
+ feedbackbasket feedback reply <id> "<drafted response>" --delivery both --reply-to support@example.com --agent
172
193
  feedbackbasket feedback update <id> --status COMPLETE --agent
173
194
  feedbackbasket feedback note <id> "Replied via CLI" --agent
174
195
  ```
175
- **Important:** If `feedback show` returns `project.replyToEmail: null`, the agent MUST either:
176
- 1. Pass `--reply-to <email>` with an explicit address, OR
177
- 2. Ask the human which reply-to email to use (the account owner's email is a reasonable default, but requires user confirmation), OR
178
- 3. Set a project default first: `feedbackbasket projects update <project> --reply-to <email>`
179
-
180
- Never silently guess a reply-to address it becomes the "From" address the customer sees.
196
+ **Important reply safety rules:**
197
+ - Before replying, the agent MUST inspect `feedback show --agent`, then ask the human which delivery method to use: `email`, `widget`, or `both`, unless the human already specified it in the current conversation.
198
+ - If `feedback show` returns `email: null`, do not use `--delivery email` or `--delivery both`. If `hasWidgetAccess: true`, use `--delivery widget`; otherwise ask the human how they want to respond.
199
+ - If `hasWidgetAccess: false`, do not use `--delivery widget` or `--delivery both`.
200
+ - If the delivery includes email and `project.replyToEmail: null`, the agent MUST ask the human which reply-to email to use before sending. Do not use the account owner's email, token owner's email, or any remembered address without explicit confirmation in the current conversation.
201
+ - After the human confirms a reply-to address, pass it explicitly with `--reply-to <email>`, or set a project default first with `feedbackbasket projects update <project> --reply-to <email>`.
202
+
203
+ Never silently guess a reply-to address. It becomes the "From" address the customer sees.
181
204
 
182
205
  ### Export for analysis
183
206
  ```bash