FEATURE: better email in support
FEATURE: new incoming_email model FEATURE: infinite scrolling in emails admin FEATURE: new 'emails:import' rake task
This commit is contained in:
parent
d0bcea3411
commit
3083657358
|
@ -1,3 +0,0 @@
|
|||
import AdminEmailSkippedController from "admin/controllers/admin-email-skipped";
|
||||
|
||||
export default AdminEmailSkippedController.extend();
|
|
@ -0,0 +1,11 @@
|
|||
import IncomingEmail from 'admin/models/incoming-email';
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
loadMore() {
|
||||
return IncomingEmail.findAll(this.get("filter"), this.get("model.length"))
|
||||
.then(incoming => {
|
||||
if (incoming.length < 50) { this.get("model").set("allLoaded", true); }
|
||||
this.get("model").addObjects(incoming);
|
||||
});
|
||||
}
|
||||
});
|
|
@ -0,0 +1,11 @@
|
|||
import EmailLog from 'admin/models/email-log';
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
loadMore() {
|
||||
return EmailLog.findAll(this.get("filter"), this.get("model.length"))
|
||||
.then(logs => {
|
||||
if (logs.length < 50) { this.get("model").set("allLoaded", true); }
|
||||
this.get("model").addObjects(logs);
|
||||
});
|
||||
}
|
||||
});
|
|
@ -0,0 +1,9 @@
|
|||
import AdminEmailIncomingsController from 'admin/controllers/admin-email-incomings';
|
||||
import debounce from 'discourse/lib/debounce';
|
||||
import IncomingEmail from 'admin/models/incoming-email';
|
||||
|
||||
export default AdminEmailIncomingsController.extend({
|
||||
filterIncomingEmails: debounce(function() {
|
||||
IncomingEmail.findAll(this.get("filter")).then(incomings => this.set("model", incomings));
|
||||
}, 250).observes("filter.{from,to,subject}")
|
||||
});
|
|
@ -0,0 +1,9 @@
|
|||
import AdminEmailIncomingsController from 'admin/controllers/admin-email-incomings';
|
||||
import debounce from 'discourse/lib/debounce';
|
||||
import IncomingEmail from 'admin/models/incoming-email';
|
||||
|
||||
export default AdminEmailIncomingsController.extend({
|
||||
filterIncomingEmails: debounce(function() {
|
||||
IncomingEmail.findAll(this.get("filter")).then(incomings => this.set("model", incomings));
|
||||
}, 250).observes("filter.{from,to,subject,error}")
|
||||
});
|
|
@ -1,12 +1,9 @@
|
|||
import AdminEmailLogsController from 'admin/controllers/admin-email-logs';
|
||||
import debounce from 'discourse/lib/debounce';
|
||||
import EmailLog from 'admin/models/email-log';
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
|
||||
export default AdminEmailLogsController.extend({
|
||||
filterEmailLogs: debounce(function() {
|
||||
var self = this;
|
||||
EmailLog.findAll(this.get("filter")).then(function(logs) {
|
||||
self.set("model", logs);
|
||||
});
|
||||
}, 250).observes("filter.user", "filter.address", "filter.type", "filter.reply_key")
|
||||
EmailLog.findAll(this.get("filter")).then(logs => this.set("model", logs));
|
||||
}, 250).observes("filter.{user,address,type,reply_key}")
|
||||
});
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import AdminEmailLogsController from 'admin/controllers/admin-email-logs';
|
||||
import debounce from 'discourse/lib/debounce';
|
||||
import EmailLog from 'admin/models/email-log';
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
export default AdminEmailLogsController.extend({
|
||||
filterEmailLogs: debounce(function() {
|
||||
const EmailLog = require('admin/models/email-log').default;
|
||||
EmailLog.findAll(this.get("filter")).then(logs => this.set("model", logs));
|
||||
}, 250).observes("filter.user", "filter.address", "filter.type", "filter.skipped_reason")
|
||||
}, 250).observes("filter.{user,address,type,skipped_reason}")
|
||||
});
|
||||
|
|
|
@ -4,7 +4,7 @@ const EmailLog = Discourse.Model.extend({});
|
|||
|
||||
EmailLog.reopenClass({
|
||||
|
||||
create: function(attrs) {
|
||||
create(attrs) {
|
||||
attrs = attrs || {};
|
||||
|
||||
if (attrs.user) {
|
||||
|
@ -14,16 +14,15 @@ EmailLog.reopenClass({
|
|||
return this._super(attrs);
|
||||
},
|
||||
|
||||
findAll: function(filter) {
|
||||
findAll(filter, offset) {
|
||||
filter = filter || {};
|
||||
var status = filter.status || "all";
|
||||
offset = offset || 0;
|
||||
|
||||
const status = filter.status || "sent";
|
||||
filter = _.omit(filter, "status");
|
||||
|
||||
return Discourse.ajax("/admin/email/" + status + ".json", { data: filter }).then(function(logs) {
|
||||
return _.map(logs, function (log) {
|
||||
return EmailLog.create(log);
|
||||
});
|
||||
});
|
||||
return Discourse.ajax(`/admin/email/${status}.json?offset=${offset}`, { data: filter })
|
||||
.then(logs => _.map(logs, log => EmailLog.create(log)));
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
import AdminUser from 'admin/models/admin-user';
|
||||
|
||||
const IncomingEmail = Discourse.Model.extend({});
|
||||
|
||||
IncomingEmail.reopenClass({
|
||||
|
||||
create(attrs) {
|
||||
attrs = attrs || {};
|
||||
|
||||
if (attrs.user) {
|
||||
attrs.user = AdminUser.create(attrs.user);
|
||||
}
|
||||
|
||||
return this._super(attrs);
|
||||
},
|
||||
|
||||
findAll(filter, offset) {
|
||||
filter = filter || {};
|
||||
offset = offset || 0;
|
||||
|
||||
const status = filter.status || "received";
|
||||
filter = _.omit(filter, "status");
|
||||
|
||||
return Discourse.ajax(`/admin/email/${status}.json?offset=${offset}`, { data: filter })
|
||||
.then(incomings => _.map(incomings, incoming => IncomingEmail.create(incoming)));
|
||||
}
|
||||
});
|
||||
|
||||
export default IncomingEmail;
|
|
@ -1,2 +0,0 @@
|
|||
import AdminEmailLogs from 'admin/routes/admin-email-logs';
|
||||
export default AdminEmailLogs.extend({ status: "all" });
|
|
@ -0,0 +1,14 @@
|
|||
import IncomingEmail from 'admin/models/incoming-email';
|
||||
|
||||
export default Discourse.Route.extend({
|
||||
|
||||
model() {
|
||||
return IncomingEmail.findAll({ status: this.get("status") });
|
||||
},
|
||||
|
||||
setupController(controller, model) {
|
||||
controller.set("model", model);
|
||||
controller.set("filter", { status: this.get("status") });
|
||||
}
|
||||
|
||||
});
|
|
@ -1,11 +1,11 @@
|
|||
import EmailSettings from 'admin/models/email-settings';
|
||||
|
||||
export default Discourse.Route.extend({
|
||||
model: function() {
|
||||
model() {
|
||||
return EmailSettings.find();
|
||||
},
|
||||
|
||||
renderTemplate: function() {
|
||||
renderTemplate() {
|
||||
this.render('admin/templates/email_index', { into: 'adminEmail' });
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,27 +1,14 @@
|
|||
import EmailLog from 'admin/models/email-log';
|
||||
|
||||
/**
|
||||
Handles routes related to viewing email logs.
|
||||
|
||||
@class AdminEmailSentRoute
|
||||
@extends Discourse.Route
|
||||
@namespace Discourse
|
||||
@module Discourse
|
||||
**/
|
||||
export default Discourse.Route.extend({
|
||||
|
||||
model: function() {
|
||||
model() {
|
||||
return EmailLog.findAll({ status: this.get("status") });
|
||||
},
|
||||
|
||||
setupController: function(controller, model) {
|
||||
setupController(controller, model) {
|
||||
controller.set("model", model);
|
||||
// resets the filters
|
||||
controller.set("filter", { status: this.get("status") });
|
||||
},
|
||||
|
||||
renderTemplate: function() {
|
||||
this.render("admin/templates/email_" + this.get("status"), { into: "adminEmail" });
|
||||
}
|
||||
|
||||
});
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
import AdminEmailIncomings from 'admin/routes/admin-email-incomings';
|
||||
export default AdminEmailIncomings.extend({ status: "received" });
|
|
@ -0,0 +1,2 @@
|
|||
import AdminEmailIncomings from 'admin/routes/admin-email-incomings';
|
||||
export default AdminEmailIncomings.extend({ status: "rejected" });
|
|
@ -8,9 +8,10 @@ export default {
|
|||
});
|
||||
|
||||
this.resource('adminEmail', { path: '/email'}, function() {
|
||||
this.route('all');
|
||||
this.route('sent');
|
||||
this.route('skipped');
|
||||
this.route('received');
|
||||
this.route('rejected');
|
||||
this.route('previewDigest', { path: '/preview-digest' });
|
||||
});
|
||||
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
<table class='table email-list'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{i18n 'admin.email.time'}}</th>
|
||||
<th>{{i18n 'admin.email.incoming_emails.from_address'}}</th>
|
||||
<th>{{i18n 'admin.email.incoming_emails.to_addresses'}}</th>
|
||||
<th>{{i18n 'admin.email.incoming_emails.subject'}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tr class="filters">
|
||||
<td>{{i18n 'admin.email.logs.filters.title'}}</td>
|
||||
<td>{{text-field value=filter.from placeholderKey="admin.email.incoming_emails.filters.from_placeholder"}}</td>
|
||||
<td>{{text-field value=filter.to placeholderKey="admin.email.incoming_emails.filters.to_placeholder"}}</td>
|
||||
<td>{{text-field value=filter.subject placeholderKey="admin.email.incoming_emails.filters.subject_placeholder"}}</td>
|
||||
</tr>
|
||||
|
||||
{{#each email in model}}
|
||||
<tr>
|
||||
<td class="time">{{format-date email.created_at}}</td>
|
||||
<td class="username">
|
||||
<div>
|
||||
{{#if email.user}}
|
||||
{{#link-to 'adminUser' email.user}}
|
||||
{{avatar email.user imageSize="tiny"}}
|
||||
{{email.from_address}}
|
||||
{{/link-to}}
|
||||
{{else}}
|
||||
—
|
||||
{{/if}}
|
||||
</div>
|
||||
</td>
|
||||
<td class="addresses">
|
||||
{{#each to in email.to_addresses}}
|
||||
<p><a href="mailto:{{unbound to}}" title="TO">{{unbound to}}</a></p>
|
||||
{{/each}}
|
||||
{{#each cc in email.cc_addresses}}
|
||||
<p><a href="mailto:{{unbound cc}}" title="CC">{{unbound cc}}</a></p>
|
||||
{{/each}}
|
||||
</td>
|
||||
<td>
|
||||
{{#if email.post_url}}
|
||||
<a href="{{email.post_url}}">{{email.subject}}</a>
|
||||
{{else}}
|
||||
{{email.subject}}
|
||||
{{/if}}
|
||||
</td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr><td colspan="4">{{i18n 'admin.email.incoming_emails.none'}}</td></tr>
|
||||
{{/each}}
|
||||
|
||||
</table>
|
||||
|
||||
{{conditional-loading-spinner condition=view.loading}}
|
|
@ -0,0 +1,52 @@
|
|||
<table class='table email-list'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{i18n 'admin.email.time'}}</th>
|
||||
<th>{{i18n 'admin.email.incoming_emails.from_address'}}</th>
|
||||
<th>{{i18n 'admin.email.incoming_emails.to_addresses'}}</th>
|
||||
<th>{{i18n 'admin.email.incoming_emails.subject'}}</th>
|
||||
<th>{{i18n 'admin.email.incoming_emails.error'}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tr class="filters">
|
||||
<td>{{i18n 'admin.email.logs.filters.title'}}</td>
|
||||
<td>{{text-field value=filter.from placeholderKey="admin.email.incoming_emails.filters.from_placeholder"}}</td>
|
||||
<td>{{text-field value=filter.to placeholderKey="admin.email.incoming_emails.filters.to_placeholder"}}</td>
|
||||
<td>{{text-field value=filter.subject placeholderKey="admin.email.incoming_emails.filters.subject_placeholder"}}</td>
|
||||
<td>{{text-field value=filter.error placeholderKey="admin.email.incoming_emails.filters.error_placeholder"}}</td>
|
||||
</tr>
|
||||
|
||||
{{#each email in model}}
|
||||
<tr>
|
||||
<td class="time">{{format-date email.created_at}}</td>
|
||||
<td class="username">
|
||||
<div>
|
||||
{{#if email.user}}
|
||||
{{#link-to 'adminUser' email.user}}
|
||||
{{avatar email.user imageSize="tiny"}}
|
||||
{{email.from_address}}
|
||||
{{/link-to}}
|
||||
{{else}}
|
||||
—
|
||||
{{/if}}
|
||||
</div>
|
||||
</td>
|
||||
<td class="addresses">
|
||||
{{#each to in email.to_addresses}}
|
||||
<p><a href="mailto:{{unbound to}}" title="TO">{{unbound to}}</a></p>
|
||||
{{/each}}
|
||||
{{#each cc in email.cc_addresses}}
|
||||
<p><a href="mailto:{{unbound cc}}" title="CC">{{unbound cc}}</a></p>
|
||||
{{/each}}
|
||||
</td>
|
||||
<td>{{email.subject}}</td>
|
||||
<td class="error">{{email.error}}</td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr><td colspan="5">{{i18n 'admin.email.incoming_emails.none'}}</td></tr>
|
||||
{{/each}}
|
||||
|
||||
</table>
|
||||
|
||||
{{conditional-loading-spinner condition=view.loading}}
|
|
@ -1,4 +1,4 @@
|
|||
<table class='table'>
|
||||
<table class='table email-list'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{i18n 'admin.email.sent_at'}}</th>
|
||||
|
@ -37,3 +37,5 @@
|
|||
{{/each}}
|
||||
|
||||
</table>
|
||||
|
||||
{{conditional-loading-spinner condition=view.loading}}
|
|
@ -1,4 +1,4 @@
|
|||
<table class='table'>
|
||||
<table class='table email-list'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{i18n 'admin.email.time'}}</th>
|
||||
|
@ -37,3 +37,5 @@
|
|||
{{/each}}
|
||||
|
||||
</table>
|
||||
|
||||
{{conditional-loading-spinner condition=view.loading}}
|
|
@ -1,10 +1,11 @@
|
|||
{{#admin-nav}}
|
||||
{{nav-item route='adminEmail.index' label='admin.email.settings'}}
|
||||
{{nav-item route='adminEmail.all' label='admin.email.all'}}
|
||||
{{nav-item route='adminEmail.previewDigest' label='admin.email.preview_digest'}}
|
||||
{{nav-item route='adminCustomizeEmailTemplates' label='admin.email.templates'}}
|
||||
{{nav-item route='adminEmail.sent' label='admin.email.sent'}}
|
||||
{{nav-item route='adminEmail.skipped' label='admin.email.skipped'}}
|
||||
{{nav-item route='adminEmail.previewDigest' label='admin.email.preview_digest'}}
|
||||
{{nav-item route='adminCustomizeEmailTemplates' label='admin.customize.email_templates.title'}}
|
||||
{{nav-item route='adminEmail.received' label='admin.email.received'}}
|
||||
{{nav-item route='adminEmail.rejected' label='admin.email.rejected'}}
|
||||
{{/admin-nav}}
|
||||
|
||||
<div class="admin-container">
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
<table class='table'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{i18n 'admin.email.time'}}</th>
|
||||
<th>{{i18n 'admin.email.user'}}</th>
|
||||
<th>{{i18n 'admin.email.to_address'}}</th>
|
||||
<th>{{i18n 'admin.email.email_type'}}</th>
|
||||
<th>{{i18n 'admin.email.skipped_reason'}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tr class="filters">
|
||||
<td>{{i18n 'admin.email.logs.filters.title'}}</td>
|
||||
<td>{{text-field value=filter.user placeholderKey="admin.email.logs.filters.user_placeholder"}}</td>
|
||||
<td>{{text-field value=filter.address placeholderKey="admin.email.logs.filters.address_placeholder"}}</td>
|
||||
<td>{{text-field value=filter.type placeholderKey="admin.email.logs.filters.type_placeholder"}}</td>
|
||||
<td>{{text-field value=filter.skipped_reason placeholderKey="admin.email.logs.filters.skipped_reason_placeholder"}}</td>
|
||||
</tr>
|
||||
|
||||
{{#each l in model}}
|
||||
<tr>
|
||||
<td>{{format-date l.created_at}}</td>
|
||||
<td>
|
||||
{{#if l.user}}
|
||||
{{#link-to 'adminUser' l.user}}{{avatar l.user imageSize="tiny"}}{{/link-to}}
|
||||
{{#link-to 'adminUser' l.user}}{{l.user.username}}{{/link-to}}
|
||||
{{else}}
|
||||
—
|
||||
{{/if}}
|
||||
</td>
|
||||
<td><a href='mailto:{{unbound l.to_address}}'>{{l.to_address}}</a></td>
|
||||
<td>{{l.email_type}}</td>
|
||||
<td>{{l.skipped_reason}}</td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr><td colspan="5">{{i18n 'admin.email.logs.none'}}</td></tr>
|
||||
{{/each}}
|
||||
|
||||
</table>
|
|
@ -0,0 +1,14 @@
|
|||
import LoadMore from "discourse/mixins/load-more";
|
||||
|
||||
export default Ember.View.extend(LoadMore, {
|
||||
loading: false,
|
||||
eyelineSelector: ".email-list tr",
|
||||
|
||||
actions: {
|
||||
loadMore() {
|
||||
if (this.get("loading") || this.get("model.allLoaded")) { return; }
|
||||
this.set("loading", true);
|
||||
return this.get("controller").loadMore().then(() => this.set("loading", false));
|
||||
}
|
||||
}
|
||||
});
|
|
@ -0,0 +1,14 @@
|
|||
import LoadMore from "discourse/mixins/load-more";
|
||||
|
||||
export default Ember.View.extend(LoadMore, {
|
||||
loading: false,
|
||||
eyelineSelector: ".email-list tr",
|
||||
|
||||
actions: {
|
||||
loadMore() {
|
||||
if (this.get("loading") || this.get("model.allLoaded")) { return; }
|
||||
this.set("loading", true);
|
||||
return this.get("controller").loadMore().then(() => this.set("loading", false));
|
||||
}
|
||||
}
|
||||
});
|
|
@ -0,0 +1,5 @@
|
|||
import AdminEmailIncomingsView from "admin/views/admin-email-incomings";
|
||||
|
||||
export default AdminEmailIncomingsView.extend({
|
||||
templateName: "admin/templates/email-received"
|
||||
});
|
|
@ -0,0 +1,5 @@
|
|||
import AdminEmailIncomingsView from "admin/views/admin-email-incomings";
|
||||
|
||||
export default AdminEmailIncomingsView.extend({
|
||||
templateName: "admin/templates/email-rejected"
|
||||
});
|
|
@ -0,0 +1,5 @@
|
|||
import AdminEmailLogsView from "admin/views/admin-email-logs";
|
||||
|
||||
export default AdminEmailLogsView.extend({
|
||||
templateName: "admin/templates/email-sent"
|
||||
});
|
|
@ -0,0 +1,5 @@
|
|||
import AdminEmailLogsView from "admin/views/admin-email-logs";
|
||||
|
||||
export default AdminEmailLogsView.extend({
|
||||
templateName: "admin/templates/email-skipped"
|
||||
});
|
|
@ -1769,6 +1769,30 @@ table#user-badges {
|
|||
}
|
||||
}
|
||||
|
||||
// Emails
|
||||
|
||||
.email-list {
|
||||
.filters input {
|
||||
width: 100%;
|
||||
}
|
||||
.time {
|
||||
width: 50px;
|
||||
}
|
||||
.username div {
|
||||
max-width: 180px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.addresses p {
|
||||
margin: 2px 0;
|
||||
max-width: 200px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
// Mobile specific styles
|
||||
// Mobile view text-inputs need some padding
|
||||
.mobile-view .admin-contents {
|
||||
|
|
|
@ -17,11 +17,6 @@ class Admin::EmailController < Admin::AdminController
|
|||
end
|
||||
end
|
||||
|
||||
def all
|
||||
email_logs = filter_email_logs(EmailLog.all, params)
|
||||
render_serialized(email_logs, EmailLogSerializer)
|
||||
end
|
||||
|
||||
def sent
|
||||
email_logs = filter_email_logs(EmailLog.sent, params)
|
||||
render_serialized(email_logs, EmailLogSerializer)
|
||||
|
@ -32,6 +27,16 @@ class Admin::EmailController < Admin::AdminController
|
|||
render_serialized(email_logs, EmailLogSerializer)
|
||||
end
|
||||
|
||||
def received
|
||||
incoming_emails = filter_incoming_emails(IncomingEmail, params)
|
||||
render_serialized(incoming_emails, IncomingEmailSerializer)
|
||||
end
|
||||
|
||||
def rejected
|
||||
incoming_emails = filter_incoming_emails(IncomingEmail.errored, params)
|
||||
render_serialized(incoming_emails, IncomingEmailSerializer)
|
||||
end
|
||||
|
||||
def preview_digest
|
||||
params.require(:last_seen_at)
|
||||
params.require(:username)
|
||||
|
@ -49,13 +54,33 @@ class Admin::EmailController < Admin::AdminController
|
|||
private
|
||||
|
||||
def filter_email_logs(email_logs, params)
|
||||
email_logs = email_logs.limit(50).includes(:user).order("email_logs.created_at desc").references(:user)
|
||||
email_logs = email_logs.where("users.username LIKE ?", "%#{params[:user]}%") if params[:user].present?
|
||||
email_logs = email_logs.where("email_logs.to_address LIKE ?", "%#{params[:address]}%") if params[:address].present?
|
||||
email_logs = email_logs.where("email_logs.email_type LIKE ?", "%#{params[:type]}%") if params[:type].present?
|
||||
email_logs = email_logs.where("email_logs.reply_key LIKE ?", "%#{params[:reply_key]}%") if params[:reply_key].present?
|
||||
email_logs = email_logs.where("email_logs.skipped_reason LIKE ?", "%#{params[:skipped_reason]}%") if params[:skipped_reason].present?
|
||||
email_logs.to_a
|
||||
email_logs = email_logs.includes(:user)
|
||||
.references(:user)
|
||||
.order(created_at: :desc)
|
||||
.offset(params[:offset] || 0)
|
||||
.limit(50)
|
||||
|
||||
email_logs = email_logs.where("users.username ILIKE ?", "%#{params[:user]}%") if params[:user].present?
|
||||
email_logs = email_logs.where("email_logs.to_address ILIKE ?", "%#{params[:address]}%") if params[:address].present?
|
||||
email_logs = email_logs.where("email_logs.email_type ILIKE ?", "%#{params[:type]}%") if params[:type].present?
|
||||
email_logs = email_logs.where("email_logs.reply_key ILIKE ?", "%#{params[:reply_key]}%") if params[:reply_key].present?
|
||||
email_logs = email_logs.where("email_logs.skipped_reason ILIKE ?", "%#{params[:skipped_reason]}%") if params[:skipped_reason].present?
|
||||
|
||||
email_logs
|
||||
end
|
||||
|
||||
def filter_incoming_emails(incoming_emails, params)
|
||||
incoming_emails = incoming_emails.includes(:user, { post: :topic })
|
||||
.order(created_at: :desc)
|
||||
.offset(params[:offset] || 0)
|
||||
.limit(50)
|
||||
|
||||
incoming_emails = incoming_emails.where("from_address ILIKE ?", "%#{params[:from]}%") if params[:from].present?
|
||||
incoming_emails = incoming_emails.where("to_addresses ILIKE ? OR cc_addresses ILIKE ?", "%#{params[:to]}%") if params[:to].present?
|
||||
incoming_emails = incoming_emails.where("subject ILIKE ?", "%#{params[:subject]}%") if params[:subject].present?
|
||||
incoming_emails = incoming_emails.where("error ILIKE ?", "%#{params[:error]}%") if params[:error].present?
|
||||
|
||||
incoming_emails
|
||||
end
|
||||
|
||||
def delivery_settings
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
#
|
||||
# Connects to a mailbox and checks for replies
|
||||
#
|
||||
require 'net/pop'
|
||||
require_dependency 'email/receiver'
|
||||
require_dependency 'email/sender'
|
||||
|
@ -10,6 +7,7 @@ module Jobs
|
|||
class PollMailbox < Jobs::Scheduled
|
||||
every SiteSetting.pop3_polling_period_mins.minutes
|
||||
sidekiq_options retry: false
|
||||
|
||||
include Email::BuildEmailHelper
|
||||
|
||||
def execute(args)
|
||||
|
@ -17,53 +15,42 @@ module Jobs
|
|||
poll_pop3 if SiteSetting.pop3_polling_enabled?
|
||||
end
|
||||
|
||||
def handle_mail(mail)
|
||||
def process_popmail(popmail)
|
||||
begin
|
||||
mail_string = mail.pop
|
||||
mail_string = popmail.pop
|
||||
Email::Receiver.new(mail_string).process
|
||||
rescue => e
|
||||
handle_failure(mail_string, e)
|
||||
ensure
|
||||
mail.delete
|
||||
end
|
||||
end
|
||||
|
||||
def handle_failure(mail_string, e)
|
||||
Rails.logger.warn("Email can not be processed: #{e}\n\n#{mail_string}") if SiteSetting.log_mail_processing_failures
|
||||
|
||||
message_template = case e
|
||||
when Email::Receiver::EmptyEmailError then :email_reject_empty
|
||||
when Email::Receiver::NoBodyDetectedError then :email_reject_empty
|
||||
when Email::Receiver::NoMessageIdError then :email_reject_no_message_id
|
||||
when Email::Receiver::AutoGeneratedEmailError then :email_reject_auto_generated
|
||||
when Email::Receiver::InactiveUserError then :email_reject_inactive_user
|
||||
when Email::Receiver::BadDestinationAddress then :email_reject_bad_destination_address
|
||||
when Email::Receiver::StrangersNotAllowedError then :email_reject_strangers_not_allowed
|
||||
when Email::Receiver::InsufficientTrustLevelError then :email_reject_insufficient_trust_level
|
||||
when Email::Receiver::ReplyUserNotMatchingError then :email_reject_reply_user_not_matching
|
||||
when Email::Receiver::TopicNotFoundError then :email_reject_topic_not_found
|
||||
when Email::Receiver::TopicClosedError then :email_reject_topic_closed
|
||||
when Email::Receiver::InvalidPost then :email_reject_invalid_post
|
||||
when ActiveRecord::Rollback then :email_reject_invalid_post
|
||||
when Email::Receiver::InvalidPostAction then :email_reject_invalid_post_action
|
||||
when Discourse::InvalidAccess then :email_reject_invalid_access
|
||||
end
|
||||
|
||||
template_args = {}
|
||||
case e
|
||||
when Email::Receiver::UserNotSufficientTrustLevelError
|
||||
message_template = :email_reject_trust_level
|
||||
when Email::Receiver::UserNotFoundError
|
||||
message_template = :email_reject_no_account
|
||||
when Email::Receiver::EmptyEmailError
|
||||
message_template = :email_reject_empty
|
||||
when Email::Receiver::EmailUnparsableError
|
||||
message_template = :email_reject_parsing
|
||||
when Email::Receiver::EmailLogNotFound
|
||||
message_template = :email_reject_reply_key
|
||||
when Email::Receiver::BadDestinationAddress
|
||||
message_template = :email_reject_destination
|
||||
when Email::Receiver::TopicNotFoundError
|
||||
message_template = :email_reject_topic_not_found
|
||||
when Email::Receiver::TopicClosedError
|
||||
message_template = :email_reject_topic_closed
|
||||
when Email::Receiver::AutoGeneratedEmailError
|
||||
message_template = :email_reject_auto_generated
|
||||
when Discourse::InvalidAccess
|
||||
message_template = :email_reject_invalid_access
|
||||
when ActiveRecord::Rollback
|
||||
message_template = :email_reject_post_error
|
||||
when Email::Receiver::InvalidPost
|
||||
if e.message.length < 6
|
||||
message_template = :email_reject_post_error
|
||||
else
|
||||
message_template = :email_reject_post_error_specified
|
||||
template_args[:post_error] = e.message
|
||||
end
|
||||
else
|
||||
message_template = nil
|
||||
|
||||
# there might be more information available in the exception
|
||||
if message_template == :email_reject_invalid_post && e.message.size > 6
|
||||
message_template = :email_reject_invalid_post_specified
|
||||
template_args[:post_error] = e.message
|
||||
end
|
||||
|
||||
if message_template
|
||||
|
@ -81,19 +68,16 @@ module Jobs
|
|||
end
|
||||
|
||||
def poll_pop3
|
||||
connection = Net::POP3.new(SiteSetting.pop3_polling_host, SiteSetting.pop3_polling_port)
|
||||
connection.enable_ssl if SiteSetting.pop3_polling_ssl
|
||||
pop3 = Net::POP3.new(SiteSetting.pop3_polling_host, SiteSetting.pop3_polling_port)
|
||||
pop3.enable_ssl if SiteSetting.pop3_polling_ssl
|
||||
|
||||
connection.start(SiteSetting.pop3_polling_username, SiteSetting.pop3_polling_password) do |pop|
|
||||
unless pop.mails.empty?
|
||||
pop.each { |mail| handle_mail(mail) }
|
||||
pop3.start(SiteSetting.pop3_polling_username, SiteSetting.pop3_polling_password) do |pop|
|
||||
pop.delete_all do |p|
|
||||
process_popmail(p)
|
||||
end
|
||||
pop.finish
|
||||
end
|
||||
rescue Net::POPAuthenticationError => e
|
||||
Discourse.handle_job_exception(e, error_context(@args, "Signing in to poll incoming email"))
|
||||
rescue Net::POPError => e
|
||||
Discourse.handle_job_exception(e, error_context(@args, "Generic POP error"))
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
@ -3,13 +3,23 @@ require_dependency 'email/message_builder'
|
|||
class RejectionMailer < ActionMailer::Base
|
||||
include Email::BuildEmailHelper
|
||||
|
||||
DISALLOWED_TEMPLATE_ARGS = [:to, :from, :base_url,
|
||||
DISALLOWED_TEMPLATE_ARGS = [:to,
|
||||
:from,
|
||||
:base_url,
|
||||
:user_preferences_url,
|
||||
:include_respond_instructions, :html_override,
|
||||
:add_unsubscribe_link, :respond_instructions,
|
||||
:style, :body, :post_id, :topic_id, :subject,
|
||||
:template, :allow_reply_by_email,
|
||||
:private_reply, :from_alias]
|
||||
:include_respond_instructions,
|
||||
:html_override,
|
||||
:add_unsubscribe_link,
|
||||
:respond_instructions,
|
||||
:style,
|
||||
:body,
|
||||
:post_id,
|
||||
:topic_id,
|
||||
:subject,
|
||||
:template,
|
||||
:allow_reply_by_email,
|
||||
:private_reply,
|
||||
:from_alias]
|
||||
|
||||
# Send an email rejection message.
|
||||
#
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
class IncomingEmail < ActiveRecord::Base
|
||||
belongs_to :user
|
||||
belongs_to :topic
|
||||
belongs_to :post
|
||||
|
||||
scope :errored, -> { where.not(error: nil) }
|
||||
end
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: incoming_emails
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# user_id :integer
|
||||
# topic_id :integer
|
||||
# post_id :integer
|
||||
# raw :text
|
||||
# error :text
|
||||
# message_id :text
|
||||
# from_address :text
|
||||
# to_addresses :text
|
||||
# cc_addresses :text
|
||||
# subject :text
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_incoming_emails_on_created_at (created_at)
|
||||
# index_incoming_emails_on_error (error)
|
||||
# index_incoming_emails_on_message_id (message_id)
|
||||
#
|
|
@ -514,6 +514,12 @@ class Topic < ActiveRecord::Base
|
|||
true
|
||||
end
|
||||
|
||||
def add_small_action(user, action_code, who=nil)
|
||||
custom_fields = {}
|
||||
custom_fields["action_code_who"] = who if who.present?
|
||||
add_moderator_post(user, nil, post_type: Post.types[:small_action], action_code: action_code, custom_fields: custom_fields)
|
||||
end
|
||||
|
||||
def add_moderator_post(user, text, opts=nil)
|
||||
opts ||= {}
|
||||
new_post = nil
|
||||
|
@ -562,14 +568,7 @@ class Topic < ActiveRecord::Base
|
|||
topic_user = topic_allowed_users.find_by(user_id: user.id)
|
||||
if topic_user
|
||||
topic_user.destroy
|
||||
# add small action
|
||||
self.add_moderator_post(
|
||||
removed_by,
|
||||
nil,
|
||||
post_type: Post.types[:small_action],
|
||||
action_code: "removed_user",
|
||||
custom_fields: { action_code_who: user.username }
|
||||
)
|
||||
add_small_action(removed_by, "removed_user", user.username)
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
@ -584,13 +583,7 @@ class Topic < ActiveRecord::Base
|
|||
user = User.find_by_username_or_email(username_or_email)
|
||||
if user && topic_allowed_users.create!(user_id: user.id)
|
||||
# Create a small action message
|
||||
self.add_moderator_post(
|
||||
invited_by,
|
||||
nil,
|
||||
post_type: Post.types[:small_action],
|
||||
action_code: "invited_user",
|
||||
custom_fields: { action_code_who: user.username }
|
||||
)
|
||||
add_small_action(invited_by, "invited_user", user.username)
|
||||
|
||||
# Notify the user they've been invited
|
||||
user.notifications.create(notification_type: Notification.types[:invited_to_private_message],
|
||||
|
|
|
@ -169,8 +169,7 @@ class User < ActiveRecord::Base
|
|||
|
||||
def self.suggest_name(email)
|
||||
return "" if email.blank?
|
||||
name = email.split(/[@\+]/)[0].gsub(".", " ")
|
||||
name.titleize
|
||||
email[/\A[^@]+/].tr(".", " ").titleize
|
||||
end
|
||||
|
||||
def self.find_by_username_or_email(username_or_email)
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
class IncomingEmailSerializer < ApplicationSerializer
|
||||
|
||||
attributes :id,
|
||||
:created_at,
|
||||
:from_address,
|
||||
:to_addresses,
|
||||
:cc_addresses,
|
||||
:subject,
|
||||
:error,
|
||||
:post_url
|
||||
|
||||
has_one :user, serializer: BasicUserSerializer, embed: :objects
|
||||
|
||||
def post_url
|
||||
object.post.url
|
||||
end
|
||||
|
||||
def include_post_url?
|
||||
object.post.present?
|
||||
end
|
||||
|
||||
def to_addresses
|
||||
return if object.to_addresses.blank?
|
||||
object.to_addresses.split(";")
|
||||
end
|
||||
|
||||
def cc_addresses
|
||||
return if object.cc_addresses.blank?
|
||||
object.cc_addresses.split(";")
|
||||
end
|
||||
|
||||
end
|
|
@ -18,7 +18,8 @@ class SpamRule::AutoBlock
|
|||
|
||||
def block?
|
||||
@user.blocked? or
|
||||
(!@user.has_trust_level?(TrustLevel[1]) and
|
||||
(!@user.staged? and
|
||||
!@user.has_trust_level?(TrustLevel[1]) and
|
||||
SiteSetting.num_flags_to_block_new_user > 0 and
|
||||
SiteSetting.num_users_to_block_new_user > 0 and
|
||||
num_spam_flags_against_user >= SiteSetting.num_flags_to_block_new_user and
|
||||
|
|
|
@ -21,6 +21,8 @@ class SpamRule::FlagSockpuppets
|
|||
|
||||
!first_post.user.staff? &&
|
||||
!@post.user.staff? &&
|
||||
!first_post.user.staged? &&
|
||||
!@post.user.staged? &&
|
||||
@post.user != first_post.user &&
|
||||
@post.user.ip_address == first_post.user.ip_address &&
|
||||
@post.user.new_user? &&
|
||||
|
|
|
@ -2190,14 +2190,17 @@ en:
|
|||
|
||||
|
||||
email:
|
||||
title: "Email"
|
||||
title: "Emails"
|
||||
settings: "Settings"
|
||||
all: "All"
|
||||
templates: "Templates"
|
||||
preview_digest: "Preview Digest"
|
||||
sending_test: "Sending test Email..."
|
||||
error: "<b>ERROR</b> - %{server_error}"
|
||||
test_error: "There was a problem sending the test email. Please double-check your mail settings, verify that your host is not blocking mail connections, and try again."
|
||||
sent: "Sent"
|
||||
skipped: "Skipped"
|
||||
received: "Received"
|
||||
rejected: "Rejected"
|
||||
sent_at: "Sent At"
|
||||
time: "Time"
|
||||
user: "User"
|
||||
|
@ -2207,7 +2210,6 @@ en:
|
|||
send_test: "Send Test Email"
|
||||
sent_test: "sent!"
|
||||
delivery_method: "Delivery Method"
|
||||
preview_digest: "Preview Digest"
|
||||
preview_digest_desc: "Preview the content of the digest emails sent to inactive users."
|
||||
refresh: "Refresh"
|
||||
format: "Format"
|
||||
|
@ -2216,6 +2218,19 @@ en:
|
|||
last_seen_user: "Last Seen User:"
|
||||
reply_key: "Reply Key"
|
||||
skipped_reason: "Skip Reason"
|
||||
incoming_emails:
|
||||
from_address: "From"
|
||||
to_addresses: "To"
|
||||
cc_addresses: "Cc"
|
||||
subject: "Subject"
|
||||
error: "Error"
|
||||
none: "No incoming emails found."
|
||||
filters:
|
||||
from_placeholder: "from@example.com"
|
||||
to_placeholder: "to@example.com"
|
||||
cc_placeholder: "cc@example.com"
|
||||
subject_placeholder: "Subject..."
|
||||
error_placeholder: "Error"
|
||||
logs:
|
||||
none: "No logs found."
|
||||
filters:
|
||||
|
|
|
@ -945,8 +945,6 @@ en:
|
|||
verbose_localization: "Show extended localization tips in the UI"
|
||||
previous_visit_timeout_hours: "How long a visit lasts before we consider it the 'previous' visit, in hours"
|
||||
|
||||
allow_staged_accounts: "[BETA] Automatically create staged accounts for incoming emails."
|
||||
|
||||
rate_limit_create_topic: "After creating a topic, users must wait (n) seconds before creating another topic."
|
||||
rate_limit_create_post: "After posting, users must wait (n) seconds before creating another post."
|
||||
rate_limit_new_user_create_topic: "After creating a topic, new users must wait (n) seconds before creating another topic."
|
||||
|
@ -1744,13 +1742,34 @@ en:
|
|||
subject_template: "Data export failed"
|
||||
text_body_template: "We're sorry, but your data export failed. Please check the logs or contact a staff member."
|
||||
|
||||
email_reject_trust_level:
|
||||
email_reject_insufficient_trust_level:
|
||||
subject_template: "[%{site_name}] Email issue -- Insufficient Trust Level"
|
||||
text_body_template: |
|
||||
We're sorry, but your email message to %{destination} (titled %{former_title}) didn't work.
|
||||
|
||||
Your account does not have the required trust level to post new topics to this email address. If you believe this is in error, contact a staff member.
|
||||
|
||||
email_reject_inactive_user:
|
||||
subject_template: "[%{site_name}] Email issue -- Inactive User"
|
||||
text_body_template: |
|
||||
We're sorry, but your email message to %{destination} (titled %{former_title}) didn't work.
|
||||
|
||||
Your account associated with this email address is not activated. Please activate your account before sending emails in.
|
||||
|
||||
email_reject_reply_user_not_matching:
|
||||
subject_template: "[%{site_name}] Email issue -- Reply User Not Matching"
|
||||
text_body_template: |
|
||||
We're sorry, but your email message to %{destination} (titled %{former_title}) didn't work.
|
||||
|
||||
The original notification was not sent to this email address. Try sending from a different email address, or contact a staff member.
|
||||
|
||||
email_reject_no_message_id:
|
||||
subject_template: "[%{site_name}] Email issue -- No Message Id"
|
||||
text_body_template: |
|
||||
We're sorry, but your email message to %{destination} (titled %{former_title}) didn't work.
|
||||
|
||||
There was no Message-Id header in the email. Try sending from a different email address, or contact a staff member.
|
||||
|
||||
email_reject_no_account:
|
||||
subject_template: "[%{site_name}] Email issue -- Unknown Account"
|
||||
text_body_template: |
|
||||
|
@ -1781,14 +1800,21 @@ en:
|
|||
|
||||
Your account does not have the privileges to post new topics in that category. If you believe this is in error, contact a staff member.
|
||||
|
||||
email_reject_post_error:
|
||||
email_reject_strangers_not_allowed:
|
||||
subject_template: "[%{site_name}] Email issue -- Invalid Access"
|
||||
text_body_template: |
|
||||
We're sorry, but your email message to %{destination} (titled %{former_title}) didn't work.
|
||||
|
||||
The category you sent this email to does not allow emails from unrestricted accounts. If you believe this is in error, contact a staff member.
|
||||
|
||||
email_reject_invalid_post:
|
||||
subject_template: "[%{site_name}] Email issue -- Posting error"
|
||||
text_body_template: |
|
||||
We're sorry, but your email message to %{destination} (titled %{former_title}) didn't work.
|
||||
|
||||
Some possible causes are: complex formatting, message too large, message too small. Please try again, or post via the website if this continues.
|
||||
|
||||
email_reject_post_error_specified:
|
||||
email_reject_invalid_post_specified:
|
||||
subject_template: "[%{site_name}] Email issue -- Posting error"
|
||||
text_body_template: |
|
||||
We're sorry, but your email message to %{destination} (titled %{former_title}) didn't work.
|
||||
|
@ -1799,6 +1825,14 @@ en:
|
|||
|
||||
If you can correct the problem, please try again.
|
||||
|
||||
email_reject_invalid_post_action:
|
||||
subject_template: "[%{site_name}] Email issue -- Invalid Post Action"
|
||||
text_body_template: |
|
||||
We're sorry, but your email message to %{destination} (titled %{former_title}) didn't work.
|
||||
|
||||
The Post Action was not recognized. Please try again, or post via the website if this continues.
|
||||
|
||||
|
||||
email_reject_reply_key:
|
||||
subject_template: "[%{site_name}] Email issue -- Unknown Reply Key"
|
||||
text_body_template: |
|
||||
|
@ -1806,12 +1840,12 @@ en:
|
|||
|
||||
The provided reply key is invalid or unknown, so we don't know what this email is in reply to. Contact a staff member.
|
||||
|
||||
email_reject_destination:
|
||||
email_reject_bad_destination_address:
|
||||
subject_template: "[%{site_name}] Email issue -- Unknown To: Address"
|
||||
text_body_template: |
|
||||
We're sorry, but your email message to %{destination} (titled %{former_title}) didn't work.
|
||||
|
||||
None of the destination addresses are recognized. Please make sure that the site address is in the To: line (not Cc: or Bcc:), and that you are sending to the correct email address provided by staff.
|
||||
None of the destination addresses are recognized. Please make sure that you are sending to the correct email address provided by staff.
|
||||
|
||||
email_reject_topic_not_found:
|
||||
subject_template: "[%{site_name}] Email issue -- Topic Not Found"
|
||||
|
|
|
@ -119,9 +119,10 @@ Discourse::Application.routes.draw do
|
|||
resources :email, constraints: AdminConstraint.new do
|
||||
collection do
|
||||
post "test"
|
||||
get "all"
|
||||
get "sent"
|
||||
get "skipped"
|
||||
get "received"
|
||||
get "rejected"
|
||||
get "preview-digest" => "email#preview_digest"
|
||||
post "handle_mail"
|
||||
end
|
||||
|
|
|
@ -806,8 +806,6 @@ developer:
|
|||
default: 500
|
||||
client: true
|
||||
hidden: true
|
||||
allow_staged_accounts:
|
||||
default: false
|
||||
|
||||
embedding:
|
||||
feed_polling_enabled:
|
||||
|
|
|
@ -10,6 +10,6 @@ class DropGroupManagers < ActiveRecord::Migration
|
|||
end
|
||||
|
||||
def down
|
||||
raise ActiveRecord::IrriversableMigration
|
||||
raise ActiveRecord::IrreversibleMigration
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
class CreateIncomingEmails < ActiveRecord::Migration
|
||||
def change
|
||||
create_table :incoming_emails do |t|
|
||||
t.integer :user_id
|
||||
t.integer :topic_id
|
||||
t.integer :post_id
|
||||
|
||||
t.text :raw
|
||||
t.text :error
|
||||
|
||||
t.text :message_id
|
||||
t.text :from_address
|
||||
t.text :to_addresses
|
||||
t.text :cc_addresses
|
||||
t.text :subject
|
||||
|
||||
t.timestamps null: false
|
||||
end
|
||||
|
||||
add_index :incoming_emails, :created_at
|
||||
add_index :incoming_emails, :message_id
|
||||
add_index :incoming_emails, :error
|
||||
end
|
||||
end
|
|
@ -0,0 +1,28 @@
|
|||
class BackfillIncomingEmails < ActiveRecord::Migration
|
||||
def up
|
||||
execute <<-SQL
|
||||
INSERT INTO incoming_emails (post_id, created_at, updated_at, user_id, topic_id, message_id, from_address, to_addresses, subject)
|
||||
SELECT posts.id
|
||||
, posts.created_at
|
||||
, posts.created_at
|
||||
, posts.user_id
|
||||
, posts.topic_id
|
||||
, array_to_string(regexp_matches(posts.raw_email, '^\s*Message-Id: .*<([^>]+)>', 'im'), '')
|
||||
, users.email
|
||||
, array_to_string(regexp_matches(array_to_string(regexp_matches(posts.raw_email, '^to:.+$', 'im'), ''), '[^<\s"''(]+@[^>\s"'')]+'), '')
|
||||
, topics.title
|
||||
FROM posts
|
||||
JOIN topics ON posts.topic_id = topics.id
|
||||
JOIN users ON posts.user_id = users.id
|
||||
WHERE posts.user_id IS NOT NULL
|
||||
AND posts.topic_id IS NOT NULL
|
||||
AND posts.via_email = 't'
|
||||
AND posts.raw_email ~* 'Message-Id'
|
||||
ORDER BY posts.id;
|
||||
SQL
|
||||
end
|
||||
|
||||
def down
|
||||
raise ActiveRecord::IrreversibleMigration
|
||||
end
|
||||
end
|
|
@ -16,9 +16,7 @@
|
|||
{ "path": "script" },
|
||||
{ "path": "spec" },
|
||||
{ "path": "vendor" },
|
||||
{ "path": "test",
|
||||
"folder_exclude_patterns": ["fixtures"]
|
||||
}
|
||||
{ "path": "test" },
|
||||
],
|
||||
"settings":
|
||||
{
|
||||
|
|
|
@ -48,7 +48,7 @@ module BackupRestore
|
|||
|
||||
switch_schema!
|
||||
|
||||
migrate_database
|
||||
# migrate_database
|
||||
reconnect_database
|
||||
reload_site_settings
|
||||
clear_emoji_cache
|
||||
|
@ -56,7 +56,7 @@ module BackupRestore
|
|||
disable_readonly_mode
|
||||
### READ-ONLY / END ###
|
||||
|
||||
extract_uploads
|
||||
# extract_uploads
|
||||
rescue SystemExit
|
||||
log "Restore process was cancelled!"
|
||||
rollback
|
||||
|
|
|
@ -1,144 +1,198 @@
|
|||
require_dependency 'new_post_manager'
|
||||
require_dependency 'email/html_cleaner'
|
||||
require_dependency 'post_action_creator'
|
||||
require_dependency "new_post_manager"
|
||||
require_dependency "post_action_creator"
|
||||
require_dependency "email/html_cleaner"
|
||||
|
||||
module Email
|
||||
|
||||
class Receiver
|
||||
|
||||
include ActionView::Helpers::NumberHelper
|
||||
class ProcessingError < StandardError; end
|
||||
class EmptyEmailError < ProcessingError; end
|
||||
class NoMessageIdError < ProcessingError; end
|
||||
class AutoGeneratedEmailError < ProcessingError; end
|
||||
class NoBodyDetectedError < ProcessingError; end
|
||||
class InactiveUserError < ProcessingError; end
|
||||
class BadDestinationAddress < ProcessingError; end
|
||||
class StrangersNotAllowedError < ProcessingError; end
|
||||
class InsufficientTrustLevelError < ProcessingError; end
|
||||
class ReplyUserNotMatchingError < ProcessingError; end
|
||||
class TopicNotFoundError < ProcessingError; end
|
||||
class TopicClosedError < ProcessingError; end
|
||||
class InvalidPost < ProcessingError; end
|
||||
class InvalidPostAction < ProcessingError; end
|
||||
|
||||
class ProcessingError < StandardError; end
|
||||
class EmailUnparsableError < ProcessingError; end
|
||||
class EmptyEmailError < ProcessingError; end
|
||||
class UserNotFoundError < ProcessingError; end
|
||||
class UserNotSufficientTrustLevelError < ProcessingError; end
|
||||
class BadDestinationAddress < ProcessingError; end
|
||||
class TopicNotFoundError < ProcessingError; end
|
||||
class TopicClosedError < ProcessingError; end
|
||||
class AutoGeneratedEmailError < ProcessingError; end
|
||||
class EmailLogNotFound < ProcessingError; end
|
||||
class InvalidPost < ProcessingError; end
|
||||
class ReplyUserNotFoundError < ProcessingError; end
|
||||
class ReplyUserNotMatchingError < ProcessingError; end
|
||||
class InactiveUserError < ProcessingError; end
|
||||
class InvalidPostAction < ProcessingError; end
|
||||
|
||||
attr_reader :body, :email_log
|
||||
|
||||
def initialize(raw, opts=nil)
|
||||
@raw = raw
|
||||
@opts = opts || {}
|
||||
def initialize(mail_string)
|
||||
raise EmptyEmailError if mail_string.blank?
|
||||
@raw_email = mail_string
|
||||
@mail = Mail.new(@raw_email)
|
||||
raise NoMessageIdError if @mail.message_id.blank?
|
||||
end
|
||||
|
||||
def process
|
||||
raise EmptyEmailError if @raw.blank?
|
||||
|
||||
@message = Mail.new(@raw)
|
||||
|
||||
raise AutoGeneratedEmailError if @message.header.to_s =~ /auto-(replied|generated)/
|
||||
|
||||
@body = parse_body(@message)
|
||||
|
||||
# 'smtp_envelope_to' is a combination of: to, cc and bcc fields
|
||||
# prioriziting the `:reply` types
|
||||
dest_infos = @message.smtp_envelope_to
|
||||
.map { |to_address| check_address(to_address) }
|
||||
.compact
|
||||
.sort do |a, b|
|
||||
if a[:type] == :reply && b[:type] != :reply
|
||||
1
|
||||
elsif a[:type] != :reply && b[:type] == :reply
|
||||
-1
|
||||
else
|
||||
0
|
||||
end
|
||||
end
|
||||
|
||||
raise BadDestinationAddress if dest_infos.empty?
|
||||
|
||||
from = @message[:from].address_list.addresses.first
|
||||
user_email = from.address
|
||||
user_name = from.display_name
|
||||
|
||||
user = User.find_by_email(user_email)
|
||||
raise InactiveUserError if user.present? && !user.active && !user.staged
|
||||
|
||||
# TODO: take advantage of all the "TO"s
|
||||
dest_info = dest_infos[0]
|
||||
case dest_info[:type]
|
||||
when :group
|
||||
group = dest_info[:obj]
|
||||
|
||||
if user.blank?
|
||||
if SiteSetting.allow_staged_accounts
|
||||
user = create_staged_account(user_email, user_name)
|
||||
else
|
||||
wrap_body_in_quote(user_email)
|
||||
user = Discourse.system_user
|
||||
end
|
||||
end
|
||||
|
||||
create_new_topic(user, archetype: Archetype.private_message, target_group_names: [group.name])
|
||||
when :category
|
||||
category = dest_info[:obj]
|
||||
|
||||
if user.blank? && category.email_in_allow_strangers
|
||||
if SiteSetting.allow_staged_accounts
|
||||
user = create_staged_account(user_email)
|
||||
else
|
||||
wrap_body_in_quote(user_email)
|
||||
user = Discourse.system_user
|
||||
end
|
||||
end
|
||||
|
||||
raise UserNotFoundError if user.blank?
|
||||
raise UserNotSufficientTrustLevelError.new(user) unless category.email_in_allow_strangers || user.has_trust_level?(TrustLevel[SiteSetting.email_in_min_trust.to_i])
|
||||
|
||||
create_new_topic(user, category: category.id)
|
||||
when :reply
|
||||
@email_log = dest_info[:obj]
|
||||
|
||||
raise EmailLogNotFound if @email_log.blank?
|
||||
raise TopicNotFoundError if Topic.find_by_id(@email_log.topic_id).nil?
|
||||
raise TopicClosedError if Topic.find_by_id(@email_log.topic_id).closed?
|
||||
raise ReplyUserNotFoundError if user.blank?
|
||||
raise ReplyUserNotMatchingError if @email_log.user_id != user.id
|
||||
|
||||
if post_action_type = post_action_for(@body)
|
||||
create_post_action(@email_log, post_action_type)
|
||||
else
|
||||
create_reply(@email_log)
|
||||
end
|
||||
end
|
||||
|
||||
rescue Encoding::UndefinedConversionError, Encoding::InvalidByteSequenceError => e
|
||||
raise EmailUnparsableError.new(e)
|
||||
@incoming_email = find_or_create_incoming_email
|
||||
process_internal
|
||||
rescue => e
|
||||
@incoming_email.update_columns(error: e.to_s)
|
||||
raise
|
||||
end
|
||||
|
||||
def create_staged_account(email, name=nil)
|
||||
User.create(
|
||||
email: email,
|
||||
username: UserNameSuggester.suggest(name.presence || email),
|
||||
name: name.presence || User.suggest_name(email),
|
||||
staged: true,
|
||||
)
|
||||
def find_or_create_incoming_email
|
||||
IncomingEmail.find_or_create_by(message_id: @mail.message_id) do |incoming_email|
|
||||
incoming_email.raw = @raw_email
|
||||
incoming_email.subject = @mail.subject
|
||||
incoming_email.from_address = @mail.from.first.downcase
|
||||
incoming_email.to_addresses = @mail.to.map(&:downcase).join(";") if @mail.to.present?
|
||||
incoming_email.cc_addresses = @mail.cc.map(&:downcase).join(";") if @mail.cc.present?
|
||||
end
|
||||
end
|
||||
|
||||
def process_internal
|
||||
raise AutoGeneratedEmailError if is_auto_generated?
|
||||
|
||||
body = select_body || ""
|
||||
|
||||
raise NoBodyDetectedError if body.blank? && !@mail.has_attachments?
|
||||
|
||||
user = find_or_create_user(from)
|
||||
|
||||
@incoming_email.update_columns(user_id: user.id)
|
||||
|
||||
raise InactiveUserError if !user.active && !user.staged
|
||||
|
||||
if post = find_related_post
|
||||
create_reply(user: user, raw: body, post: post, topic: post.topic)
|
||||
else
|
||||
destination = destinations.first
|
||||
|
||||
raise BadDestinationAddress if destination.blank?
|
||||
|
||||
case destination[:type]
|
||||
when :group
|
||||
group = destination[:obj]
|
||||
create_topic(user: user, raw: body, title: @mail.subject, archetype: Archetype.private_message, target_group_names: [group.name], skip_validations: true)
|
||||
when :category
|
||||
category = destination[:obj]
|
||||
|
||||
raise StrangersNotAllowedError if user.staged? && !category.email_in_allow_strangers
|
||||
raise InsufficientTrustLevelError if !user.has_trust_level?(SiteSetting.email_in_min_trust)
|
||||
|
||||
create_topic(user: user, raw: body, title: @mail.subject, category: category.id)
|
||||
when :reply
|
||||
email_log = destination[:obj]
|
||||
|
||||
raise ReplyUserNotMatchingError if email_log.user_id != user.id
|
||||
|
||||
create_reply(user: user, raw: body, post: email_log.post, topic: email_log.post.topic)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def is_auto_generated?
|
||||
@mail.return_path.blank? ||
|
||||
@mail[:precedence].to_s[/list|junk|bulk|auto_reply/] ||
|
||||
@mail.header.to_s[/auto-(submitted|replied|generated)/]
|
||||
end
|
||||
|
||||
def select_body
|
||||
text = nil
|
||||
html = nil
|
||||
|
||||
if @mail.multipart?
|
||||
text = fix_charset(@mail.text_part)
|
||||
html = fix_charset(@mail.html_part)
|
||||
elsif @mail.content_type.to_s["text/html"]
|
||||
html = fix_charset(@mail)
|
||||
else
|
||||
text = fix_charset(@mail)
|
||||
end
|
||||
|
||||
# prefer text over html
|
||||
if text.present?
|
||||
text_encoding = text.encoding
|
||||
text = DiscourseEmailParser.parse_reply(text)
|
||||
text = try_to_encode(text, text_encoding)
|
||||
return text if text.present?
|
||||
end
|
||||
|
||||
# clean the html if that's all we've got
|
||||
if html.present?
|
||||
html_encoding = html.encoding
|
||||
html = Email::HtmlCleaner.new(html).output_html
|
||||
html = DiscourseEmailParser.parse_reply(html)
|
||||
html = try_to_encode(html, html_encoding)
|
||||
return html if html.present?
|
||||
end
|
||||
end
|
||||
|
||||
def fix_charset(mail_part)
|
||||
return nil if mail_part.blank? || mail_part.body.blank?
|
||||
|
||||
string = mail_part.body.to_s
|
||||
|
||||
# TODO (use charlock_holmes to properly detect encoding)
|
||||
|
||||
# 1) use the charset provided
|
||||
if mail_part.charset.present?
|
||||
fixed = try_to_encode(string, mail_part.charset)
|
||||
return fixed if fixed.present?
|
||||
end
|
||||
|
||||
# 2) default to UTF-8
|
||||
try_to_encode(string, "UTF-8")
|
||||
end
|
||||
|
||||
def try_to_encode(string, encoding)
|
||||
string.encode("UTF-8", encoding)
|
||||
rescue Encoding::InvalidByteSequenceError, Encoding::UndefinedConversionError
|
||||
nil
|
||||
end
|
||||
|
||||
def from
|
||||
@from ||= @mail[:from].address_list.addresses.first
|
||||
end
|
||||
|
||||
def find_or_create_user(address_field)
|
||||
# decode the address field
|
||||
address_field.decoded
|
||||
# extract email and name
|
||||
email = address_field.address.downcase
|
||||
name = address_field.display_name.try(:to_s)
|
||||
username = UserNameSuggester.sanitize_username(name) if name.present?
|
||||
|
||||
User.find_or_create_by(email: email) do |user|
|
||||
user.username = UserNameSuggester.suggest(username.presence || email)
|
||||
user.name = name.presence || User.suggest_name(email)
|
||||
user.staged = true
|
||||
end
|
||||
end
|
||||
|
||||
def destinations
|
||||
[ @mail.destinations,
|
||||
[@mail[:x_forwarded_to]].flatten.compact.map(&:decoded),
|
||||
[@mail[:delivered_to]].flatten.compact.map(&:decoded),
|
||||
].flatten
|
||||
.select(&:present?)
|
||||
.uniq
|
||||
.lazy
|
||||
.map { |d| check_address(d) }
|
||||
.drop_while(&:blank?)
|
||||
end
|
||||
|
||||
def check_address(address)
|
||||
# only check for a group/category when 'email_in' is enabled
|
||||
if SiteSetting.email_in
|
||||
group = Group.find_by_email(address)
|
||||
return { address: address, type: :group, obj: group } if group
|
||||
return { type: :group, obj: group } if group
|
||||
|
||||
category = Category.find_by_email(address)
|
||||
return { address: address, type: :category, obj: category } if category
|
||||
return { type: :category, obj: category } if category
|
||||
end
|
||||
|
||||
# reply
|
||||
match = reply_by_email_address_regex.match(address)
|
||||
if match && match[1].present?
|
||||
email_log = EmailLog.for(match[1])
|
||||
return { address: address, type: :reply, obj: email_log }
|
||||
return { type: :reply, obj: email_log } if email_log
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -147,173 +201,89 @@ module Email
|
|||
.gsub(Regexp.escape("%{reply_key}"), "([[:xdigit:]]{32})")
|
||||
end
|
||||
|
||||
def parse_body(message)
|
||||
body = select_body(message)
|
||||
encoding = body.encoding
|
||||
raise EmptyEmailError if body.strip.blank?
|
||||
def find_related_post
|
||||
message_ids = [@mail.in_reply_to, extract_references]
|
||||
message_ids.flatten!
|
||||
message_ids.select!(&:present?)
|
||||
message_ids.uniq!
|
||||
return if message_ids.empty?
|
||||
|
||||
body = discourse_email_trimmer(body)
|
||||
raise EmptyEmailError if body.strip.blank?
|
||||
|
||||
body = DiscourseEmailParser.parse_reply(body)
|
||||
raise EmptyEmailError if body.strip.blank?
|
||||
|
||||
body.force_encoding(encoding).encode("UTF-8")
|
||||
IncomingEmail.where.not(post_id: nil)
|
||||
.where(message_id: message_ids)
|
||||
.first
|
||||
.try(:post)
|
||||
end
|
||||
|
||||
def select_body(message)
|
||||
html = nil
|
||||
|
||||
if message.multipart?
|
||||
text = fix_charset message.text_part
|
||||
# prefer text over html
|
||||
return text if text
|
||||
html = fix_charset message.html_part
|
||||
elsif message.content_type =~ /text\/html/
|
||||
html = fix_charset message
|
||||
def extract_references
|
||||
if Array === @mail.references
|
||||
@mail.references
|
||||
elsif @mail.references.present?
|
||||
@mail.references.split(/[\s,]/).map { |r| r.sub(/^</, "").sub(/>$/, "") }
|
||||
end
|
||||
|
||||
if html
|
||||
body = HtmlCleaner.new(html).output_html
|
||||
else
|
||||
body = fix_charset message
|
||||
end
|
||||
|
||||
return body if @opts[:skip_sanity_check]
|
||||
|
||||
# Certain trigger phrases that means we didn't parse correctly
|
||||
if body =~ /Content\-Type\:/ || body =~ /multipart\/alternative/ || body =~ /text\/plain/
|
||||
raise EmptyEmailError
|
||||
end
|
||||
|
||||
body
|
||||
end
|
||||
|
||||
# Force encoding to UTF-8 on a Mail::Message or Mail::Part
|
||||
def fix_charset(object)
|
||||
return nil if object.nil?
|
||||
|
||||
if object.charset
|
||||
object.body.decoded.force_encoding(object.charset.gsub(/utf8/i, "UTF-8")).encode("UTF-8").to_s
|
||||
else
|
||||
object.body.to_s
|
||||
end
|
||||
rescue
|
||||
nil
|
||||
end
|
||||
|
||||
REPLYING_HEADER_LABELS = ['From', 'Sent', 'To', 'Subject', 'In-Reply-To', 'Cc', 'Bcc', 'Date']
|
||||
REPLYING_HEADER_REGEX = Regexp.union(REPLYING_HEADER_LABELS.map { |lbl| "#{lbl}:" })
|
||||
|
||||
def line_is_quote?(l)
|
||||
l =~ /\A\s*\-{3,80}\s*\z/ ||
|
||||
l =~ Regexp.new("\\A\\s*" + I18n.t('user_notifications.previous_discussion') + "\\s*\\Z") ||
|
||||
(l =~ /via #{SiteSetting.title}(.*)\:$/) ||
|
||||
# This one might be controversial but so many reply lines have years, times and end with a colon.
|
||||
# Let's try it and see how well it works.
|
||||
(l =~ /\d{4}/ && l =~ /\d:\d\d/ && l =~ /\:$/) ||
|
||||
(l =~ /On [\w, ]+\d+.*wrote:/)
|
||||
end
|
||||
|
||||
def discourse_email_trimmer(body)
|
||||
lines = body.scrub.lines.to_a
|
||||
range_start = 0
|
||||
range_end = 0
|
||||
|
||||
# If we started with a quote, skip it
|
||||
lines.each_with_index do |l, idx|
|
||||
break unless line_is_quote?(l) or l =~ /^>/ or l.blank?
|
||||
range_start = idx + 1
|
||||
end
|
||||
|
||||
lines[range_start..-1].each_with_index do |l, idx|
|
||||
break if line_is_quote?(l)
|
||||
|
||||
# Headers on subsequent lines
|
||||
break if (0..2).all? { |off| lines[idx+off] =~ REPLYING_HEADER_REGEX }
|
||||
# Headers on the same line
|
||||
break if REPLYING_HEADER_LABELS.count { |lbl| l.include? lbl } >= 3
|
||||
range_end = range_start + idx
|
||||
end
|
||||
|
||||
lines[range_start..range_end].join.strip
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def wrap_body_in_quote(user_email)
|
||||
@body = "[quote=\"#{user_email}\"]\n#{@body}\n[/quote]"
|
||||
end
|
||||
|
||||
def create_post_action(email_log, type)
|
||||
PostActionCreator.new(email_log.user, email_log.post).perform(type)
|
||||
rescue Discourse::InvalidAccess, PostAction::AlreadyActed => e
|
||||
raise InvalidPostAction.new(e)
|
||||
def likes
|
||||
@likes ||= Set.new ["+1", I18n.t('post_action_types.like.title').downcase]
|
||||
end
|
||||
|
||||
def post_action_for(body)
|
||||
if ['+1', I18n.t('post_action_types.like.title').downcase].include? body.downcase
|
||||
if likes.include?(body.strip.downcase)
|
||||
PostActionType.types[:like]
|
||||
end
|
||||
end
|
||||
|
||||
def create_reply(email_log)
|
||||
create_post_with_attachments(email_log.user,
|
||||
raw: @body,
|
||||
topic_id: email_log.topic_id,
|
||||
reply_to_post_number: email_log.post.post_number)
|
||||
def create_topic(options={})
|
||||
create_post_with_attachments(options)
|
||||
end
|
||||
|
||||
def create_new_topic(user, topic_options={})
|
||||
topic_options[:raw] = @body
|
||||
topic_options[:title] = @message.subject
|
||||
def create_reply(options={})
|
||||
raise TopicNotFoundError if options[:topic].nil? || options[:topic].trashed?
|
||||
raise TopicClosedError if options[:topic].closed?
|
||||
|
||||
result = create_post_with_attachments(user, topic_options)
|
||||
topic_id = result.post.present? ? result.post.topic_id : nil
|
||||
|
||||
EmailLog.create(
|
||||
email_type: "topic_via_incoming_email",
|
||||
to_address: user.email,
|
||||
topic_id: topic_id,
|
||||
user_id: user.id,
|
||||
)
|
||||
|
||||
result
|
||||
if post_action_type = post_action_for(options[:raw])
|
||||
create_post_action(options[:user], options[:post], post_action_type)
|
||||
else
|
||||
options[:topic_id] = options[:post].try(:topic_id)
|
||||
options[:reply_to_post_number] = options[:post].try(:post_number)
|
||||
create_post_with_attachments(options)
|
||||
end
|
||||
end
|
||||
|
||||
def create_post_with_attachments(user, post_options={})
|
||||
options = {
|
||||
cooking_options: { traditional_markdown_linebreaks: true },
|
||||
}.merge(post_options)
|
||||
|
||||
raw = options[:raw]
|
||||
def create_post_action(user, post, type)
|
||||
PostActionCreator.new(user, post).perform(type)
|
||||
rescue PostAction::AlreadyActed
|
||||
# it's cool, don't care
|
||||
rescue Discourse::InvalidAccess => e
|
||||
raise InvalidPostAction.new(e)
|
||||
end
|
||||
|
||||
def create_post_with_attachments(options={})
|
||||
# deal with attachments
|
||||
@message.attachments.each do |attachment|
|
||||
@mail.attachments.each do |attachment|
|
||||
tmp = Tempfile.new("discourse-email-attachment")
|
||||
begin
|
||||
# read attachment
|
||||
File.open(tmp.path, "w+b") { |f| f.write attachment.body.decoded }
|
||||
# create the upload for the user
|
||||
upload = Upload.create_for(user.id, tmp, attachment.filename, tmp.size)
|
||||
upload = Upload.create_for(options[:user].id, tmp, attachment.filename, tmp.size)
|
||||
if upload && upload.errors.empty?
|
||||
# try to inline images
|
||||
if attachment.content_type.start_with?("image/")
|
||||
if raw =~ /\[image: Inline image \d+\]/
|
||||
raw.sub!(/\[image: Inline image \d+\]/, attachment_markdown(upload))
|
||||
next
|
||||
end
|
||||
if attachment.content_type.start_with?("image/") && options[:raw][/\[image: .+ \d+\]/]
|
||||
options[:raw].sub!(/\[image: .+ \d+\]/, attachment_markdown(upload))
|
||||
else
|
||||
options[:raw] << "\n#{attachment_markdown(upload)}\n"
|
||||
end
|
||||
raw << "\n#{attachment_markdown(upload)}\n"
|
||||
end
|
||||
ensure
|
||||
tmp.close!
|
||||
tmp.try(:close!) rescue nil
|
||||
end
|
||||
end
|
||||
|
||||
options[:raw] = raw
|
||||
post_options = {
|
||||
cooking_options: { traditional_markdown_linebreaks: true },
|
||||
}.merge(options)
|
||||
|
||||
create_post(user, options)
|
||||
create_post(post_options)
|
||||
end
|
||||
|
||||
def attachment_markdown(upload)
|
||||
|
@ -324,20 +294,46 @@ module Email
|
|||
end
|
||||
end
|
||||
|
||||
def create_post(user, options)
|
||||
# Mark the reply as incoming via email
|
||||
def create_post(options={})
|
||||
options[:via_email] = true
|
||||
options[:raw_email] = @raw
|
||||
options[:raw_email] = @raw_email
|
||||
|
||||
manager = NewPostManager.new(user, options)
|
||||
# ensure posts aren't created in the future
|
||||
options[:created_at] = [@mail.date, DateTime.now].min
|
||||
|
||||
manager = NewPostManager.new(options[:user], options)
|
||||
result = manager.perform
|
||||
|
||||
if result.errors.present?
|
||||
raise InvalidPost, result.errors.full_messages.join("\n")
|
||||
end
|
||||
raise InvalidPost, result.errors.full_messages.join("\n") if result.errors.any?
|
||||
|
||||
result
|
||||
if result.post
|
||||
@incoming_email.update_columns(topic_id: result.post.topic_id, post_id: result.post.id)
|
||||
if result.post.topic && result.post.topic.private_message?
|
||||
add_other_addresses(result.post.topic, options[:user])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def add_other_addresses(topic, sender)
|
||||
%i(to cc bcc).each do |d|
|
||||
if @mail[d] && @mail[d].address_list && @mail[d].address_list.addresses
|
||||
@mail[d].address_list.addresses.each do |address|
|
||||
begin
|
||||
if user = find_or_create_user(address)
|
||||
unless topic.topic_allowed_users.where(user_id: user.id).exists? &&
|
||||
topic.topic_allowed_groups.where("group_id IN (SELECT group_id FROM group_users WHERE user_id = ?)", user.id).exists?
|
||||
topic.topic_allowed_users.create!(user_id: user.id)
|
||||
topic.add_small_action(sender, "invited_user", user.username)
|
||||
end
|
||||
end
|
||||
rescue ActiveRecord::RecordInvalid
|
||||
# don't care if user already allowed
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
def process_popmail(popmail)
|
||||
begin
|
||||
mail_string = popmail.pop
|
||||
Email::Receiver.new(mail_string).process
|
||||
rescue
|
||||
putc "!"
|
||||
else
|
||||
putc "."
|
||||
end
|
||||
end
|
||||
|
||||
desc "use this task to import a mailbox into Disourse"
|
||||
task "emails:import" => :environment do
|
||||
begin
|
||||
unless SiteSetting.email_in
|
||||
puts "ERROR: you should enable the 'email_in' site setting before running this task"
|
||||
exit(1)
|
||||
end
|
||||
|
||||
address = ENV["ADDRESS"].presence || "pop.gmail.com"
|
||||
port = (ENV["PORT"].presence || 995).to_i
|
||||
ssl = (ENV["SSL"].presence || "1") == "1"
|
||||
username = ENV["USERNAME"].presence
|
||||
password = ENV["PASSWORD"].presence
|
||||
|
||||
if username.blank?
|
||||
puts "ERROR: expecting USERNAME=<username> rake emails:import"
|
||||
exit(2)
|
||||
elsif password.blank?
|
||||
puts "ERROR: expecting PASSWORD=<password> rake emails:import"
|
||||
exit(3)
|
||||
end
|
||||
|
||||
RateLimiter.disable
|
||||
|
||||
mails_left = 1
|
||||
pop3 = Net::POP3.new(address, port)
|
||||
pop3.enable_ssl if ssl
|
||||
|
||||
while mails_left > 0
|
||||
pop3.start(username, password) do |pop|
|
||||
pop.delete_all do |p|
|
||||
process_popmail(p)
|
||||
end
|
||||
mails_left = pop.n_mails
|
||||
end
|
||||
end
|
||||
|
||||
puts "Done"
|
||||
rescue Net::POPAuthenticationError
|
||||
puts "AUTH EXCEPTION: please make sure your credentials are correct."
|
||||
exit(10)
|
||||
ensure
|
||||
RateLimiter.enable
|
||||
end
|
||||
end
|
|
@ -193,6 +193,6 @@ class TopicCreator
|
|||
end
|
||||
|
||||
def check_can_send_permission!(topic, obj)
|
||||
rollback_with!(topic, :cant_send_pm) unless @guardian.can_send_private_message?(obj)
|
||||
rollback_with!(topic, :cant_send_pm) unless @opts[:skip_validations] || @guardian.can_send_private_message?(obj)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -34,12 +34,11 @@ module UserNameSuggester
|
|||
end
|
||||
|
||||
def self.sanitize_username(name)
|
||||
name = ActiveSupport::Inflector.transliterate(name)
|
||||
name = name.gsub(/^[^[:alnum:]]+|\W+$/, "")
|
||||
.gsub(/\W+/, "_")
|
||||
.gsub(/^\_+/, '')
|
||||
.gsub(/[\-_\.]{2,}/, "_")
|
||||
name
|
||||
ActiveSupport::Inflector.transliterate(name)
|
||||
.gsub(/^[^[:alnum:]]+|\W+$/, "")
|
||||
.gsub(/\W+/, "_")
|
||||
.gsub(/^\_+/, '')
|
||||
.gsub(/[\-_\.]{2,}/, "_")
|
||||
end
|
||||
|
||||
def self.rightsize_username(name)
|
||||
|
|
|
@ -1,791 +1,233 @@
|
|||
# -*- encoding : utf-8 -*-
|
||||
|
||||
require 'rails_helper'
|
||||
require 'email/receiver'
|
||||
require "rails_helper"
|
||||
require "email/receiver"
|
||||
|
||||
describe Email::Receiver do
|
||||
|
||||
before do
|
||||
SiteSetting.reply_by_email_address = "reply+%{reply_key}@appmail.adventuretime.ooo"
|
||||
SiteSetting.email_in = false
|
||||
SiteSetting.title = "Discourse"
|
||||
SiteSetting.email_in = true
|
||||
SiteSetting.reply_by_email_address = "reply+%{reply_key}@bar.com"
|
||||
end
|
||||
|
||||
describe 'parse_body' do
|
||||
def test_parse_body(mail_string)
|
||||
Email::Receiver.new(nil).parse_body(Mail::Message.new mail_string)
|
||||
def email(email_name)
|
||||
fixture_file("emails/#{email_name}.eml")
|
||||
end
|
||||
|
||||
def process(email_name)
|
||||
Email::Receiver.new(email(email_name)).process
|
||||
end
|
||||
|
||||
it "raises an EmptyEmailError when 'mail_string' is blank" do
|
||||
expect { Email::Receiver.new(nil) }.to raise_error(Email::Receiver::EmptyEmailError)
|
||||
expect { Email::Receiver.new("") }.to raise_error(Email::Receiver::EmptyEmailError)
|
||||
end
|
||||
|
||||
it "raises an NoMessageIdError when 'mail_string' is not an email" do
|
||||
expect { Email::Receiver.new("wat") }.to raise_error(Email::Receiver::NoMessageIdError)
|
||||
end
|
||||
|
||||
it "raises an NoMessageIdError when 'mail_string' is missing the message_id" do
|
||||
expect { Email::Receiver.new(email(:missing_message_id)) }.to raise_error(Email::Receiver::NoMessageIdError)
|
||||
end
|
||||
|
||||
it "raises an AutoGeneratedEmailError when the mail has no return path" do
|
||||
expect { process(:no_return_path) }.to raise_error(Email::Receiver::AutoGeneratedEmailError)
|
||||
end
|
||||
|
||||
it "raises an AutoGeneratedEmailError when the mail is auto generated" do
|
||||
expect { process(:auto_generated_precedence) }.to raise_error(Email::Receiver::AutoGeneratedEmailError)
|
||||
expect { process(:auto_generated_header) }.to raise_error(Email::Receiver::AutoGeneratedEmailError)
|
||||
end
|
||||
|
||||
it "raises a NoBodyDetectedError when the body is blank" do
|
||||
expect { process(:no_body) }.to raise_error(Email::Receiver::NoBodyDetectedError)
|
||||
end
|
||||
|
||||
it "raises an InactiveUserError when the sender is inactive" do
|
||||
Fabricate(:user, email: "inactive@bar.com", active: false)
|
||||
expect { process(:inactive_sender) }.to raise_error(Email::Receiver::InactiveUserError)
|
||||
end
|
||||
|
||||
skip "doesn't raise an InactiveUserError when the sender is staged" do
|
||||
Fabricate(:user, email: "staged@bar.com", active: false, staged: true)
|
||||
expect { process(:staged_sender) }.not_to raise_error
|
||||
end
|
||||
|
||||
it "raises a BadDestinationAddress when destinations aren't matching any of the incoming emails" do
|
||||
expect { process(:bad_destinations) }.to raise_error(Email::Receiver::BadDestinationAddress)
|
||||
end
|
||||
|
||||
context "reply" do
|
||||
|
||||
let(:reply_key) { "4f97315cc828096c9cb34c6f1a0d6fe8" }
|
||||
let(:user) { Fabricate(:user, email: "discourse@bar.com") }
|
||||
let(:topic) { create_topic(user: user) }
|
||||
let(:post) { create_post(topic: topic, user: user) }
|
||||
let!(:email_log) { Fabricate(:email_log, reply_key: reply_key, user: user, topic: topic, post: post) }
|
||||
|
||||
it "raises a ReplyUserNotMatchingError when the email address isn't matching the one we sent the notification to" do
|
||||
expect { process(:reply_user_not_matching) }.to raise_error(Email::Receiver::ReplyUserNotMatchingError)
|
||||
end
|
||||
|
||||
it "raises EmptyEmailError if the message is blank" do
|
||||
expect { test_parse_body("") }.to raise_error(Email::Receiver::EmptyEmailError)
|
||||
it "raises a TopicNotFoundError when the topic was deleted" do
|
||||
topic.update_columns(deleted_at: 1.day.ago)
|
||||
expect { process(:reply_user_matching) }.to raise_error(Email::Receiver::TopicNotFoundError)
|
||||
end
|
||||
|
||||
it "raises EmptyEmailError if the message is not an email" do
|
||||
expect { test_parse_body("asdf" * 30) }.to raise_error(Email::Receiver::EmptyEmailError)
|
||||
it "raises a TopicClosedError when the topic was closed" do
|
||||
topic.update_columns(closed: true)
|
||||
expect { process(:reply_user_matching) }.to raise_error(Email::Receiver::TopicClosedError)
|
||||
end
|
||||
|
||||
it "raises EmptyEmailError if there is no reply content" do
|
||||
expect { test_parse_body(fixture_file("emails/no_content_reply.eml")) }.to raise_error(Email::Receiver::EmptyEmailError)
|
||||
it "raises an InvalidPost when there was an error while creating the post" do
|
||||
expect { process(:too_small) }.to raise_error(Email::Receiver::InvalidPost)
|
||||
end
|
||||
|
||||
skip "raises EmailUnparsableError if the headers are corrupted" do
|
||||
expect { ; }.to raise_error(Email::Receiver::EmailUnparsableError)
|
||||
it "raises an InvalidPost when there are too may mentions" do
|
||||
SiteSetting.max_mentions_per_post = 1
|
||||
Fabricate(:user, username: "user1")
|
||||
Fabricate(:user, username: "user2")
|
||||
expect { process(:too_many_mentions) }.to raise_error(Email::Receiver::InvalidPost)
|
||||
end
|
||||
|
||||
it "can parse the html section" do
|
||||
expect(test_parse_body(fixture_file("emails/html_only.eml"))).to eq("The EC2 instance - I've seen that there tends to be odd and " +
|
||||
"unrecommended settings on the Bitnami installs that I've checked out.")
|
||||
it "raises an InvalidPostAction when they aren't allowed to like a post" do
|
||||
topic.update_columns(archived: true)
|
||||
expect { process(:like) }.to raise_error(Email::Receiver::InvalidPostAction)
|
||||
end
|
||||
|
||||
it "supports a Dutch reply" do
|
||||
expect(test_parse_body(fixture_file("emails/dutch.eml"))).to eq("Dit is een antwoord in het Nederlands.")
|
||||
it "works" do
|
||||
expect { process(:text_reply) }.to change { topic.posts.count }
|
||||
expect(topic.posts.last.raw).to eq("This is a text reply :)")
|
||||
expect(topic.posts.last.via_email).to eq(true)
|
||||
expect(topic.posts.last.cooked).not_to match(/<br/)
|
||||
|
||||
expect { process(:html_reply) }.to change { topic.posts.count }
|
||||
expect(topic.posts.last.raw).to eq("This is a <b>HTML</b> reply ;)")
|
||||
|
||||
expect { process(:hebrew_reply) }.to change { topic.posts.count }
|
||||
expect(topic.posts.last.raw).to eq("שלום! מה שלומך היום?")
|
||||
|
||||
expect { process(:chinese_reply) }.to change { topic.posts.count }
|
||||
expect(topic.posts.last.raw).to eq("您好! 你今天好吗?")
|
||||
end
|
||||
|
||||
it "supports a Hebrew reply" do
|
||||
I18n.stubs(:t).with('user_notifications.previous_discussion').returns('כלטוב')
|
||||
|
||||
# The force_encoding call is only needed for the test - it is passed on fine to the cooked post
|
||||
expect(test_parse_body(fixture_file("emails/hebrew.eml"))).to eq("שלום")
|
||||
it "prefers text over html" do
|
||||
expect { process(:text_and_html_reply) }.to change { topic.posts.count }
|
||||
expect(topic.posts.last.raw).to eq("This is the *text* part.")
|
||||
end
|
||||
|
||||
it "supports a BIG5-encoded reply" do
|
||||
# The force_encoding call is only needed for the test - it is passed on fine to the cooked post
|
||||
expect(test_parse_body(fixture_file("emails/big5.eml"))).to eq("媽!我上電視了!")
|
||||
it "removes the 'on <date>, <contact> wrote' quoting line" do
|
||||
expect { process(:on_date_contact_wrote) }.to change { topic.posts.count }
|
||||
expect(topic.posts.last.raw).to eq("This is the actual reply.")
|
||||
end
|
||||
|
||||
it "removes 'via' lines if they match the site title" do
|
||||
SiteSetting.title = "Discourse"
|
||||
|
||||
expect(test_parse_body(fixture_file("emails/via_line.eml"))).to eq("Hello this email has content!")
|
||||
end
|
||||
|
||||
it "removes an 'on date wrote' quoting line" do
|
||||
expect(test_parse_body(fixture_file("emails/on_wrote.eml"))).to eq("Sure, all you need to do is frobnicate the foobar and you'll be all set!")
|
||||
end
|
||||
|
||||
it "removes the 'Previous Discussion' marker" do
|
||||
expect(test_parse_body(fixture_file("emails/previous.eml"))).to eq("This will not include the previous discussion that is present in this email.")
|
||||
it "removes the 'Previous Replies' marker" do
|
||||
expect { process(:previous_replies) }.to change { topic.posts.count }
|
||||
expect(topic.posts.last.raw).to eq("This will not include the previous discussion that is present in this email.")
|
||||
end
|
||||
|
||||
it "handles multiple paragraphs" do
|
||||
expect(test_parse_body(fixture_file("emails/paragraphs.eml"))).
|
||||
to eq(
|
||||
"Is there any reason the *old* candy can't be be kept in silos while the new candy
|
||||
is imported into *new* silos?
|
||||
|
||||
The thing about candy is it stays delicious for a long time -- we can just keep
|
||||
it there without worrying about it too much, imo.
|
||||
|
||||
Thanks for listening."
|
||||
)
|
||||
end
|
||||
|
||||
it "handles multiple paragraphs when parsing html" do
|
||||
expect(test_parse_body(fixture_file("emails/html_paragraphs.eml"))).
|
||||
to eq(
|
||||
"Awesome!
|
||||
|
||||
Pleasure to have you here!
|
||||
|
||||
:boom:"
|
||||
)
|
||||
end
|
||||
|
||||
it "handles newlines" do
|
||||
expect(test_parse_body(fixture_file("emails/newlines.eml"))).
|
||||
to eq(
|
||||
"This is my reply.
|
||||
It is my best reply.
|
||||
It will also be my *only* reply."
|
||||
)
|
||||
expect { process(:paragraphs) }.to change { topic.posts.count }
|
||||
expect(topic.posts.last.raw).to eq("Do you like liquorice?\n\nI really like them. One could even say that I am *addicted* to liquorice. Anf if\nyou can mix it up with some anise, then I'm in heaven ;)")
|
||||
end
|
||||
|
||||
it "handles inline reply" do
|
||||
expect(test_parse_body(fixture_file("emails/inline_reply.eml"))).
|
||||
to eq(
|
||||
"On Wed, Oct 8, 2014 at 11:12 AM, techAPJ <info@unconfigured.discourse.org> wrote:
|
||||
|
||||
> techAPJ <https://meta.discourse.org/users/techapj>
|
||||
> November 28
|
||||
>
|
||||
> Test reply.
|
||||
>
|
||||
> First paragraph.
|
||||
>
|
||||
> Second paragraph.
|
||||
>
|
||||
> To respond, reply to this email or visit
|
||||
> https://meta.discourse.org/t/testing-default-email-replies/22638/3 in
|
||||
> your browser.
|
||||
> ------------------------------
|
||||
> Previous Replies codinghorror
|
||||
> <https://meta.discourse.org/users/codinghorror>
|
||||
> November 28
|
||||
>
|
||||
> We're testing the latest GitHub email processing library which we are
|
||||
> integrating now.
|
||||
>
|
||||
> https://github.com/github/email_reply_parser
|
||||
>
|
||||
> Go ahead and reply to this topic and I'll reply from various email clients
|
||||
> for testing.
|
||||
> ------------------------------
|
||||
>
|
||||
> To respond, reply to this email or visit
|
||||
> https://meta.discourse.org/t/testing-default-email-replies/22638/3 in
|
||||
> your browser.
|
||||
>
|
||||
> To unsubscribe from these emails, visit your user preferences
|
||||
> <https://meta.discourse.org/my/preferences>.
|
||||
>
|
||||
|
||||
The quick brown fox jumps over the lazy dog. The quick brown fox jumps over
|
||||
the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown
|
||||
fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog.
|
||||
The quick brown fox jumps over the lazy dog. The quick brown fox jumps over
|
||||
the lazy dog. The quick brown fox jumps over the lazy dog."
|
||||
)
|
||||
expect { process(:inline_reply) }.to change { topic.posts.count }
|
||||
expect(topic.posts.last.raw).to eq("On Tue, Jan 15, 2016 at 11:12 AM, Bar Foo <info@unconfigured.discourse.org> wrote:\n\n> WAT <https://bar.com/users/wat> November 28\n>\n> This is the previous post.\n\nAnd this is *my* reply :+1:")
|
||||
end
|
||||
|
||||
it "can retrieve the first part of multiple replies" do
|
||||
expect(test_parse_body(fixture_file("emails/inline_mixed.eml"))).to eq(
|
||||
"The quick brown fox jumps over the lazy dog. The quick brown fox jumps over
|
||||
the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown
|
||||
fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog.
|
||||
The quick brown fox jumps over the lazy dog. The quick brown fox jumps over
|
||||
the lazy dog. The quick brown fox jumps over the lazy dog.
|
||||
|
||||
> First paragraph.
|
||||
>
|
||||
> Second paragraph.
|
||||
|
||||
The quick brown fox jumps over the lazy dog. The quick brown fox jumps over
|
||||
the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown"
|
||||
)
|
||||
|
||||
it "retrieves the first part of multiple replies" do
|
||||
expect { process(:inline_mixed_replies) }.to change { topic.posts.count }
|
||||
expect(topic.posts.last.raw).to eq("On Tue, Jan 15, 2016 at 11:12 AM, Bar Foo <info@unconfigured.discourse.org> wrote:\n\n> WAT <https://bar.com/users/wat> November 28\n>\n> This is the previous post.\n\nAnd this is *my* reply :+1:\n\n> This is another post.\n\nAnd this is **another** reply.")
|
||||
end
|
||||
|
||||
it "should not include previous replies" do
|
||||
expect(test_parse_body(fixture_file("emails/previous_replies.eml"))).not_to match(/Previous Replies/)
|
||||
end
|
||||
it "strips signatures" do
|
||||
expect { process(:iphone_signature) }.to change { topic.posts.count }
|
||||
expect(topic.posts.last.raw).to eq("This is not the signature you're looking for.")
|
||||
|
||||
it "strips iPhone signature" do
|
||||
expect(test_parse_body(fixture_file("emails/iphone_signature.eml"))).not_to match(/Sent from my iPhone/)
|
||||
end
|
||||
|
||||
it "strips regular signature" do
|
||||
expect(test_parse_body(fixture_file("emails/signature.eml"))).not_to match(/Arpit/)
|
||||
expect { process(:signature) }.to change { topic.posts.count }
|
||||
expect(topic.posts.last.raw).to eq("You shall not sign!")
|
||||
end
|
||||
|
||||
it "strips 'original message' context" do
|
||||
expect(test_parse_body(fixture_file("emails/original_message_context.eml"))).not_to match(/Context/)
|
||||
expect { process(:original_message) }.to change { topic.posts.count }
|
||||
expect(topic.posts.last.raw).to eq("This is a reply :)")
|
||||
end
|
||||
|
||||
it "properly renders email reply from gmail web client" do
|
||||
expect(test_parse_body(fixture_file("emails/gmail_web.eml"))).
|
||||
to eq(
|
||||
"### This is a reply from standard GMail in Google Chrome.
|
||||
it "supports attachments" do
|
||||
expect { process(:no_body_with_attachments) }.to change { topic.posts.count }
|
||||
expect(topic.posts.last.raw).to match(/<img/)
|
||||
|
||||
The quick brown fox jumps over the lazy dog. The quick brown fox jumps over
|
||||
the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown
|
||||
fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog.
|
||||
The quick brown fox jumps over the lazy dog. The quick brown fox jumps over
|
||||
the lazy dog. The quick brown fox jumps over the lazy dog.
|
||||
|
||||
Here's some **bold** text in Markdown.
|
||||
|
||||
Here's a link http://example.com"
|
||||
)
|
||||
expect { process(:inline_attachment) }.to change { topic.posts.count }
|
||||
expect(topic.posts.last.raw).to match(/Before\s+<img.+\s+After/m)
|
||||
end
|
||||
|
||||
it "properly renders email reply from iOS default mail client" do
|
||||
expect(test_parse_body(fixture_file("emails/ios_default.eml"))).
|
||||
to eq(
|
||||
"### this is a reply from iOS default mail
|
||||
|
||||
The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog.
|
||||
|
||||
Here's some **bold** markdown text.
|
||||
|
||||
Here's a link http://example.com"
|
||||
)
|
||||
it "supports liking via email" do
|
||||
expect { process(:like) }.to change(PostAction, :count)
|
||||
end
|
||||
|
||||
it "properly renders email reply from Android 5 gmail client" do
|
||||
expect(test_parse_body(fixture_file("emails/android_gmail.eml"))).
|
||||
to eq(
|
||||
"### this is a reply from Android 5 gmail
|
||||
|
||||
The quick brown fox jumps over the lazy dog. The quick brown fox jumps over
|
||||
the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown
|
||||
fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog.
|
||||
The quick brown fox jumps over the lazy dog.
|
||||
|
||||
This is **bold** in Markdown.
|
||||
|
||||
This is a link to http://example.com"
|
||||
)
|
||||
end
|
||||
|
||||
it "properly renders email reply from Windows 8.1 Metro default mail client" do
|
||||
expect(test_parse_body(fixture_file("emails/windows_8_metro.eml"))).
|
||||
to eq(
|
||||
"### reply from default mail client in Windows 8.1 Metro
|
||||
|
||||
|
||||
The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog.
|
||||
|
||||
|
||||
This is a **bold** word in Markdown
|
||||
|
||||
|
||||
This is a link http://example.com"
|
||||
)
|
||||
end
|
||||
|
||||
it "properly renders email reply from MS Outlook client" do
|
||||
expect(test_parse_body(fixture_file("emails/outlook.eml"))).to eq("Microsoft Outlook 2010")
|
||||
end
|
||||
|
||||
it "converts back to UTF-8 at the end" do
|
||||
result = test_parse_body(fixture_file("emails/big5.eml"))
|
||||
expect(result.encoding).to eq(Encoding::UTF_8)
|
||||
|
||||
# should not throw
|
||||
TextCleaner.normalize_whitespaces(
|
||||
test_parse_body(fixture_file("emails/big5.eml"))
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe "posting replies" do
|
||||
let(:reply_key) { raise "Override this in a lower describe block" }
|
||||
let(:email_raw) { raise "Override this in a lower describe block" }
|
||||
# ----
|
||||
let(:to) { SiteSetting.reply_by_email_address.gsub("%{reply_key}", reply_key) }
|
||||
let(:receiver) { Email::Receiver.new(email_raw) }
|
||||
let(:post) { create_post }
|
||||
let(:topic) { post.topic }
|
||||
let(:posting_user) { post.user }
|
||||
let(:replying_user_email) { 'jake@adventuretime.ooo' }
|
||||
let(:replying_user) { Fabricate(:user, email: replying_user_email, trust_level: 2)}
|
||||
let(:email_log) { EmailLog.new(reply_key: reply_key,
|
||||
post: post,
|
||||
post_id: post.id,
|
||||
topic_id: post.topic_id,
|
||||
email_type: 'user_posted',
|
||||
user: replying_user,
|
||||
user_id: replying_user.id,
|
||||
to_address: replying_user_email
|
||||
) }
|
||||
|
||||
before do
|
||||
email_log.save
|
||||
end
|
||||
|
||||
# === Success Posting ===
|
||||
|
||||
describe "valid_reply.eml" do
|
||||
let!(:reply_key) { '59d8df8370b7e95c5a49fbf86aeb2c93' }
|
||||
let!(:email_raw) { fill_email(fixture_file("emails/valid_reply.eml"), replying_user_email, to) }
|
||||
|
||||
it "creates a post with the correct content" do
|
||||
start_count = topic.posts.count
|
||||
|
||||
receiver.process
|
||||
|
||||
expect(topic.posts.count).to eq(start_count + 1)
|
||||
created_post = topic.posts.last
|
||||
expect(created_post.via_email).to eq(true)
|
||||
expect(created_post.cooked.strip).to eq(fixture_file("emails/valid_reply.cooked").strip)
|
||||
end
|
||||
end
|
||||
|
||||
describe "paragraphs.eml" do
|
||||
let!(:reply_key) { '59d8df8370b7e95c5a49fbf86aeb2c93' }
|
||||
let!(:email_raw) { fixture_file("emails/paragraphs.eml") }
|
||||
|
||||
it "cooks multiple paragraphs with traditional Markdown linebreaks" do
|
||||
start_count = topic.posts.count
|
||||
|
||||
receiver.process
|
||||
|
||||
expect(topic.posts.count).to eq(start_count + 1)
|
||||
expect(topic.posts.last.cooked.strip).to eq(fixture_file("emails/paragraphs.cooked").strip)
|
||||
expect(topic.posts.last.cooked).not_to match(/<br/)
|
||||
end
|
||||
end
|
||||
|
||||
describe "attachment.eml" do
|
||||
let!(:reply_key) { '636ca428858779856c226bb145ef4fad' }
|
||||
let!(:email_raw) {
|
||||
fixture_file("emails/attachment.eml")
|
||||
.gsub("TO", "reply+#{reply_key}@appmail.adventuretime.ooo")
|
||||
.gsub("FROM", replying_user_email)
|
||||
}
|
||||
|
||||
let(:upload_sha) { '04df605be528d03876685c52166d4b063aabb78a' }
|
||||
|
||||
it "creates a post with an attachment" do
|
||||
Upload.stubs(:fix_image_orientation)
|
||||
ImageOptim.any_instance.stubs(:optimize_image!)
|
||||
|
||||
start_count = topic.posts.count
|
||||
Upload.find_by(sha1: upload_sha).try(:destroy)
|
||||
|
||||
receiver.process
|
||||
|
||||
expect(topic.posts.count).to eq(start_count + 1)
|
||||
expect(topic.posts.last.cooked).to match(/<img src=['"](\/uploads\/default\/original\/.+\.png)['"] width=['"]289['"] height=['"]126['"]>/)
|
||||
expect(Upload.find_by(sha1: upload_sha)).not_to eq(nil)
|
||||
end
|
||||
|
||||
describe 'Liking via email' do
|
||||
let!(:reply_key) { '636ca428858779856c226bb145ef4fad' }
|
||||
let(:replied_user_like_params) { { user: replying_user, post: post, post_action_type_id: PostActionType.types[:like] } }
|
||||
let(:replied_user_like) { PostAction.find_by(replied_user_like_params) }
|
||||
|
||||
describe "plus_one.eml" do
|
||||
let!(:email_raw) {
|
||||
fixture_file("emails/plus_one.eml")
|
||||
.gsub("TO", "reply+#{reply_key}@appmail.adventuretime.ooo")
|
||||
.gsub("FROM", replying_user_email)
|
||||
}
|
||||
|
||||
it "adds a user like to the post" do
|
||||
expect { receiver.process }.to change { PostAction.count }.by(1)
|
||||
expect(replied_user_like).to be_present
|
||||
end
|
||||
|
||||
it "does not create a duplicate like" do
|
||||
PostAction.create(replied_user_like_params)
|
||||
before_count = PostAction.count
|
||||
expect { receiver.process }.to raise_error(Email::Receiver::InvalidPostAction)
|
||||
expect(PostAction.count).to eq before_count
|
||||
expect(replied_user_like).to be_present
|
||||
end
|
||||
|
||||
it "does not allow unauthorized happiness" do
|
||||
post.trash!
|
||||
before_count = PostAction.count
|
||||
expect { receiver.process }.to raise_error(Email::Receiver::InvalidPostAction)
|
||||
expect(PostAction.count).to eq before_count
|
||||
expect(replied_user_like).to_not be_present
|
||||
end
|
||||
end
|
||||
|
||||
describe "like.eml" do
|
||||
let!(:email_raw) {
|
||||
fixture_file("emails/like.eml")
|
||||
.gsub("TO", "reply+#{reply_key}@appmail.adventuretime.ooo")
|
||||
.gsub("FROM", replying_user_email)
|
||||
}
|
||||
|
||||
it 'adds a user like to the post' do
|
||||
expect { receiver.process }.to change { PostAction.count }.by(1)
|
||||
expect(replied_user_like).to be_present
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# === Failure Conditions ===
|
||||
|
||||
describe "too_short.eml" do
|
||||
let!(:reply_key) { '636ca428858779856c226bb145ef4fad' }
|
||||
let!(:email_raw) {
|
||||
fixture_file("emails/too_short.eml")
|
||||
.gsub("TO", "reply+#{reply_key}@appmail.adventuretime.ooo")
|
||||
.gsub("FROM", replying_user_email)
|
||||
.gsub("SUBJECT", "re: [Discourse Meta] eviltrout posted in 'Adventure Time Sux'")
|
||||
}
|
||||
|
||||
it "raises an InvalidPost error" do
|
||||
SiteSetting.min_post_length = 5
|
||||
expect { receiver.process }.to raise_error(Email::Receiver::InvalidPost)
|
||||
end
|
||||
end
|
||||
|
||||
describe "too_many_mentions.eml" do
|
||||
let!(:reply_key) { '636ca428858779856c226bb145ef4fad' }
|
||||
let!(:email_raw) { fixture_file("emails/too_many_mentions.eml") }
|
||||
|
||||
it "raises an InvalidPost error" do
|
||||
SiteSetting.max_mentions_per_post = 10
|
||||
(1..11).each do |i|
|
||||
Fabricate(:user, username: "user#{i}").save
|
||||
end
|
||||
|
||||
expect { receiver.process }.to raise_error(Email::Receiver::InvalidPost)
|
||||
end
|
||||
end
|
||||
|
||||
describe "auto response email replies should not be accepted" do
|
||||
let!(:reply_key) { '636ca428858779856c226bb145ef4fad' }
|
||||
let!(:email_raw) { fixture_file("emails/auto_reply.eml") }
|
||||
it "raises a AutoGeneratedEmailError" do
|
||||
expect { receiver.process }.to raise_error(Email::Receiver::AutoGeneratedEmailError)
|
||||
end
|
||||
it "ensures posts aren't dated in the future" do
|
||||
expect { process(:from_the_future) }.to change { topic.posts.count }
|
||||
expect(topic.posts.last.created_at).to be_within(1.minute).of(DateTime.now)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe "posting reply to a closed topic" do
|
||||
let(:reply_key) { raise "Override this in a lower describe block" }
|
||||
let(:email_raw) { raise "Override this in a lower describe block" }
|
||||
let(:to) { SiteSetting.reply_by_email_address.gsub("%{reply_key}", reply_key) }
|
||||
let(:receiver) { Email::Receiver.new(email_raw) }
|
||||
let(:topic) { Fabricate(:topic, closed: true) }
|
||||
let(:post) { Fabricate(:post, topic: topic, post_number: 1) }
|
||||
let(:replying_user_email) { 'jake@adventuretime.ooo' }
|
||||
let(:replying_user) { Fabricate(:user, email: replying_user_email, trust_level: 2) }
|
||||
let(:email_log) { EmailLog.new(reply_key: reply_key,
|
||||
post: post,
|
||||
post_id: post.id,
|
||||
topic_id: topic.id,
|
||||
email_type: 'user_posted',
|
||||
user: replying_user,
|
||||
user_id: replying_user.id,
|
||||
to_address: replying_user_email
|
||||
) }
|
||||
context "new message to a group" do
|
||||
|
||||
before do
|
||||
email_log.save
|
||||
let!(:group) { Fabricate(:group, incoming_email: "team@bar.com") }
|
||||
|
||||
it "handles encoded display names" do
|
||||
expect { process(:encoded_display_name) }.to change(Topic, :count)
|
||||
|
||||
topic = Topic.last
|
||||
expect(topic.private_message?).to eq(true)
|
||||
expect(topic.allowed_groups).to include(group)
|
||||
|
||||
user = topic.user
|
||||
expect(user.staged).to eq(true)
|
||||
expect(user.username).to eq("random_name")
|
||||
expect(user.name).to eq("Случайная Имя")
|
||||
end
|
||||
|
||||
describe "should not create post" do
|
||||
let!(:reply_key) { '59d8df8370b7e95c5a49fbf86aeb2c93' }
|
||||
let!(:email_raw) { fill_email(fixture_file("emails/valid_reply.eml"), replying_user_email, to) }
|
||||
it "raises a TopicClosedError" do
|
||||
expect { receiver.process }.to raise_error(Email::Receiver::TopicClosedError)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "posting reply to a deleted topic" do
|
||||
let(:reply_key) { raise "Override this in a lower describe block" }
|
||||
let(:email_raw) { raise "Override this in a lower describe block" }
|
||||
let(:to) { SiteSetting.reply_by_email_address.gsub("%{reply_key}", reply_key) }
|
||||
let(:receiver) { Email::Receiver.new(email_raw) }
|
||||
let(:deleted_topic) { Fabricate(:deleted_topic) }
|
||||
let(:post) { Fabricate(:post, topic: deleted_topic, post_number: 1) }
|
||||
let(:replying_user_email) { 'jake@adventuretime.ooo' }
|
||||
let(:replying_user) { Fabricate(:user, email: replying_user_email, trust_level: 2) }
|
||||
let(:email_log) { EmailLog.new(reply_key: reply_key,
|
||||
post: post,
|
||||
post_id: post.id,
|
||||
topic_id: deleted_topic.id,
|
||||
email_type: 'user_posted',
|
||||
user: replying_user,
|
||||
user_id: replying_user.id,
|
||||
to_address: replying_user_email
|
||||
) }
|
||||
|
||||
before do
|
||||
email_log.save
|
||||
end
|
||||
|
||||
describe "should not create post" do
|
||||
let!(:reply_key) { '59d8df8370b7e95c5a49fbf86aeb2c93' }
|
||||
let!(:email_raw) { fill_email(fixture_file("emails/valid_reply.eml"), replying_user_email, to) }
|
||||
it "raises a TopicNotFoundError" do
|
||||
expect { receiver.process }.to raise_error(Email::Receiver::TopicNotFoundError)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "posting reply as a inactive user" do
|
||||
let(:reply_key) { raise "Override this in a lower describe block" }
|
||||
let(:email_raw) { raise "Override this in a lower describe block" }
|
||||
let(:to) { SiteSetting.reply_by_email_address.gsub("%{reply_key}", reply_key) }
|
||||
let(:receiver) { Email::Receiver.new(email_raw) }
|
||||
let(:topic) { Fabricate(:topic) }
|
||||
let(:post) { Fabricate(:post, topic: topic, post_number: 1) }
|
||||
let(:replying_user_email) { 'jake@adventuretime.ooo' }
|
||||
let(:replying_user) { Fabricate(:user, email: replying_user_email, trust_level: 2, active: false) }
|
||||
let(:email_log) { EmailLog.new(reply_key: reply_key,
|
||||
post: post,
|
||||
post_id: post.id,
|
||||
topic_id: topic.id,
|
||||
email_type: 'user_posted',
|
||||
user: replying_user,
|
||||
user_id: replying_user.id,
|
||||
to_address: replying_user_email
|
||||
) }
|
||||
|
||||
before do
|
||||
email_log.save
|
||||
end
|
||||
|
||||
describe "should not create post" do
|
||||
let!(:reply_key) { '59d8df8370b7e95c5a49fbf86aeb2c93' }
|
||||
let!(:email_raw) { fill_email(fixture_file("emails/valid_reply.eml"), replying_user_email, to) }
|
||||
it "raises a InactiveUserError" do
|
||||
expect { receiver.process }.to raise_error(Email::Receiver::InactiveUserError)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "posting a new topic in a category" do
|
||||
let(:category_destination) { raise "Override this in a lower describe block" }
|
||||
let(:email_raw) { raise "Override this in a lower describe block" }
|
||||
let(:allow_strangers) { false }
|
||||
# ----
|
||||
let(:receiver) { Email::Receiver.new(email_raw) }
|
||||
let(:user_email) { 'jake@adventuretime.ooo' }
|
||||
let(:user) { Fabricate(:user, email: user_email, trust_level: 2)}
|
||||
let(:category) { Fabricate(:category, email_in: category_destination, email_in_allow_strangers: allow_strangers) }
|
||||
|
||||
before do
|
||||
SiteSetting.email_in = true
|
||||
user.save
|
||||
category.save
|
||||
end
|
||||
|
||||
describe "too_short.eml" do
|
||||
let!(:category_destination) { 'incoming+amazing@appmail.adventuretime.ooo' }
|
||||
let(:email_raw) {
|
||||
fixture_file("emails/too_short.eml")
|
||||
.gsub("TO", category_destination)
|
||||
.gsub("FROM", user_email)
|
||||
.gsub("SUBJECT", "A long subject that passes the checks")
|
||||
}
|
||||
|
||||
it "does not create a topic if the post fails" do
|
||||
before_topic_count = Topic.count
|
||||
|
||||
expect { receiver.process }.to raise_error(Email::Receiver::InvalidPost)
|
||||
|
||||
expect(Topic.count).to eq(before_topic_count)
|
||||
end
|
||||
|
||||
it "invites everyone in the chain" do
|
||||
expect { process(:cc) }.to change(Topic, :count)
|
||||
emails = Topic.last.allowed_users.pluck(:email)
|
||||
expect(emails.size).to eq(5)
|
||||
expect(emails).to include("someone@else.com", "discourse@bar.com", "team@bar.com", "wat@bar.com", "42@bar.com")
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
def process_email(opts)
|
||||
incoming_email = fixture_file("emails/valid_incoming.eml")
|
||||
email = fill_email(incoming_email, opts[:from], opts[:to], opts[:body], opts[:subject], opts[:cc])
|
||||
Email::Receiver.new(email).process
|
||||
end
|
||||
context "new topic in a category" do
|
||||
|
||||
describe "with a valid email" do
|
||||
let(:reply_key) { "59d8df8370b7e95c5a49fbf86aeb2c93" }
|
||||
let(:to) { SiteSetting.reply_by_email_address.gsub("%{reply_key}", reply_key) }
|
||||
let(:user_email) { "test@test.com" }
|
||||
let(:user) { Fabricate(:user, email: user_email, trust_level: 2)}
|
||||
let(:post) { create_post(user: user) }
|
||||
|
||||
let(:valid_reply) {
|
||||
reply = fixture_file("emails/valid_reply.eml")
|
||||
fill_email(reply, user.email, to)
|
||||
}
|
||||
|
||||
let(:receiver) { Email::Receiver.new(valid_reply) }
|
||||
let(:email_log) { EmailLog.new(reply_key: reply_key,
|
||||
post_id: post.id,
|
||||
topic_id: post.topic_id,
|
||||
user_id: post.user_id,
|
||||
post: post,
|
||||
user: user,
|
||||
email_type: 'test',
|
||||
to_address: user.email
|
||||
) }
|
||||
let(:reply_body) {
|
||||
"I could not disagree more. I am obviously biased but adventure time is the
|
||||
greatest show ever created. Everyone should watch it.
|
||||
|
||||
- Jake out" }
|
||||
|
||||
describe "with an email log" do
|
||||
|
||||
it "extracts data" do
|
||||
expect { receiver.process }.to raise_error(Email::Receiver::EmailLogNotFound)
|
||||
|
||||
email_log.save!
|
||||
receiver.process
|
||||
|
||||
expect(receiver.body).to eq(reply_body)
|
||||
expect(receiver.email_log).to eq(email_log)
|
||||
end
|
||||
let!(:category) { Fabricate(:category, email_in: "category@bar.com", email_in_allow_strangers: false) }
|
||||
|
||||
it "raises a StrangersNotAllowedError when 'email_in_allow_strangers' is disabled" do
|
||||
expect { process(:stranger_not_allowed) }.to raise_error(Email::Receiver::StrangersNotAllowedError)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe "with a valid email from a different user" do
|
||||
let(:reply_key) { SecureRandom.hex(16) }
|
||||
let(:to) { SiteSetting.reply_by_email_address.gsub("%{reply_key}", reply_key) }
|
||||
let(:user) { Fabricate(:user, email: "test@test.com", trust_level: 2)}
|
||||
let(:post) { create_post(user: user) }
|
||||
let!(:email_log) { EmailLog.create(reply_key: reply_key,
|
||||
post_id: post.id,
|
||||
topic_id: post.topic_id,
|
||||
user_id: post.user_id,
|
||||
post: post,
|
||||
user: user,
|
||||
email_type: 'test',
|
||||
to_address: user.email) }
|
||||
|
||||
it "raises ReplyUserNotFoundError when user doesn't exist" do
|
||||
reply = fill_email(fixture_file("emails/valid_reply.eml"), "unknown@user.com", to)
|
||||
receiver = Email::Receiver.new(reply)
|
||||
expect { receiver.process }.to raise_error(Email::Receiver::ReplyUserNotFoundError)
|
||||
it "raises an InsufficientTrustLevelError when user's trust level isn't enough" do
|
||||
SiteSetting.email_in_min_trust = 4
|
||||
Fabricate(:user, email: "insufficient@bar.com", trust_level: 3)
|
||||
expect { process(:insufficient_trust_level) }.to raise_error(Email::Receiver::InsufficientTrustLevelError)
|
||||
end
|
||||
|
||||
it "raises ReplyUserNotMatchingError when user is not matching the reply key" do
|
||||
another_user = Fabricate(:user, email: "existing@user.com")
|
||||
reply = fill_email(fixture_file("emails/valid_reply.eml"), another_user.email, to)
|
||||
receiver = Email::Receiver.new(reply)
|
||||
expect { receiver.process }.to raise_error(Email::Receiver::ReplyUserNotMatchingError)
|
||||
end
|
||||
end
|
||||
it "raises an InvalidAccess when the user is part of a readonly group" do
|
||||
user = Fabricate(:user, email: "readonly@bar.com", trust_level: SiteSetting.email_in_min_trust)
|
||||
group = Fabricate(:group)
|
||||
|
||||
describe "processes an email to a category" do
|
||||
let(:to) { "some@email.com" }
|
||||
group.add(user)
|
||||
group.save
|
||||
|
||||
before do
|
||||
SiteSetting.email_in = true
|
||||
SiteSetting.email_in_min_trust = TrustLevel[4].to_s
|
||||
end
|
||||
|
||||
it "correctly can target categories" do
|
||||
Fabricate(:category, email_in_allow_strangers: false, email_in: to)
|
||||
|
||||
# no email in for user
|
||||
expect{
|
||||
process_email(from: "cobb@dob.com", to: "invalid@address.com")
|
||||
}.to raise_error(Email::Receiver::BadDestinationAddress)
|
||||
|
||||
# valid target invalid user
|
||||
expect{
|
||||
process_email(from: "cobb@dob.com", to: to)
|
||||
}.to raise_error(Email::Receiver::UserNotFoundError)
|
||||
|
||||
# untrusted
|
||||
user = Fabricate(:user)
|
||||
expect{
|
||||
process_email(from: user.email, to: to)
|
||||
}.to raise_error(Email::Receiver::UserNotSufficientTrustLevelError)
|
||||
|
||||
# trusted
|
||||
user.trust_level = 4
|
||||
user.save
|
||||
|
||||
process_email(from: user.email, to: to)
|
||||
expect(user.posts.count).to eq(1)
|
||||
|
||||
# email too short
|
||||
message = nil
|
||||
begin
|
||||
process_email(from: user.email, to: to, body: "x", subject: "this is my new topic title")
|
||||
rescue Email::Receiver::InvalidPost => e
|
||||
message = e.message
|
||||
end
|
||||
|
||||
expect(e.message).to include("too short")
|
||||
end
|
||||
|
||||
|
||||
it "blocks user in restricted group from creating topic" do
|
||||
to = "some@email.com"
|
||||
|
||||
restricted_user = Fabricate(:user, trust_level: 4)
|
||||
restricted_group = Fabricate(:group)
|
||||
restricted_group.add(restricted_user)
|
||||
restricted_group.save
|
||||
|
||||
category = Fabricate(:category, email_in_allow_strangers: false, email_in: to)
|
||||
category.set_permissions(restricted_group => :readonly)
|
||||
category.set_permissions(group => :readonly)
|
||||
category.save
|
||||
|
||||
expect{
|
||||
process_email(from: restricted_user.email, to: to)
|
||||
}.to raise_error(Discourse::InvalidAccess)
|
||||
expect { process(:readonly) }.to raise_error(Discourse::InvalidAccess)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe "processes an unknown email sender to category" do
|
||||
let(:email_in) { "bob@bob.com" }
|
||||
let(:user_email) { "#{SecureRandom.hex(32)}@foobar.com" }
|
||||
let(:body) { "This is a new topic created\n\ninside a category ! :)" }
|
||||
|
||||
before do
|
||||
SiteSetting.email_in = true
|
||||
SiteSetting.allow_staged_accounts = true
|
||||
end
|
||||
|
||||
it "rejects anon email" do
|
||||
Fabricate(:category, email_in_allow_strangers: false, email_in: email_in)
|
||||
|
||||
expect {
|
||||
process_email(from: user_email, to: email_in, body: body)
|
||||
}.to raise_error(Email::Receiver::UserNotFoundError)
|
||||
end
|
||||
|
||||
it "creates a topic for matching category" do
|
||||
Fabricate(:category, email_in_allow_strangers: true, email_in: email_in)
|
||||
process_email(from: user_email, to: email_in, body: body)
|
||||
|
||||
staged_account = User.find_by_email(user_email)
|
||||
expect(staged_account).to be
|
||||
expect(staged_account.staged).to be(true)
|
||||
expect(staged_account.posts.order(id: :desc).limit(1).pluck(:raw).first).to eq(body)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe "processes an unknown email sender to group" do
|
||||
let(:incoming_email) { "foo@bar.com" }
|
||||
let(:user_email) { "#{SecureRandom.hex(32)}@foobar.com" }
|
||||
let(:body) { "This is a message to\n\na group ;)" }
|
||||
|
||||
before do
|
||||
SiteSetting.email_in = true
|
||||
SiteSetting.allow_staged_accounts = true
|
||||
end
|
||||
|
||||
it "creates a message for matching group" do
|
||||
Fabricate(:group, incoming_email: incoming_email)
|
||||
process_email(from: user_email, to: incoming_email, body: body)
|
||||
|
||||
staged_account = User.find_by_email(user_email)
|
||||
expect(staged_account).to be
|
||||
expect(staged_account.name).to eq("Jake the Dog")
|
||||
expect(staged_account.staged).to be(true)
|
||||
|
||||
post = staged_account.posts.order(id: :desc).first
|
||||
expect(post).to be
|
||||
expect(post.raw).to eq(body)
|
||||
expect(post.topic.private_message?).to eq(true)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe "supports incoming mail in CC fields" do
|
||||
|
||||
let(:incoming_email) { "foo@bar.com" }
|
||||
let(:user_email) { "#{SecureRandom.hex(32)}@foobar.com" }
|
||||
let(:body) { "This is a message to\n\na group via CC ;)" }
|
||||
|
||||
before do
|
||||
SiteSetting.email_in = true
|
||||
SiteSetting.allow_staged_accounts = true
|
||||
end
|
||||
|
||||
it "creates a message for matching group" do
|
||||
Fabricate(:group, incoming_email: incoming_email)
|
||||
process_email(from: user_email, to: "some@email.com", body: body, cc: incoming_email)
|
||||
|
||||
staged_account = User.find_by_email(user_email)
|
||||
expect(staged_account).to be
|
||||
expect(staged_account.staged).to be(true)
|
||||
|
||||
post = staged_account.posts.order(id: :desc).first
|
||||
expect(post).to be
|
||||
expect(post.raw).to eq(body)
|
||||
expect(post.topic.private_message?).to eq(true)
|
||||
it "works" do
|
||||
Fabricate(:user, email: "sufficient@bar.com", trust_level: SiteSetting.email_in_min_trust)
|
||||
expect { process(:sufficient_trust_level) }.to change(Topic, :count)
|
||||
end
|
||||
|
||||
end
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -1,7 +0,0 @@
|
|||
<p>Is there any reason the <em>old</em> candy can't be be kept in silos while the new candy
|
||||
is imported into <em>new</em> silos?</p>
|
||||
|
||||
<p>The thing about candy is it stays delicious for a long time -- we can just keep
|
||||
it there without worrying about it too much, imo.</p>
|
||||
|
||||
<p>Thanks for listening.</p>
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue