Contact
CoCalc Logo Icon
StoreFeaturesDocsShareSupport News AboutSign UpSign In
| Download
Views: 39539
1
###############################################################################
2
#
3
# CoCalc: Collaborative Calculation in the Cloud
4
#
5
# Copyright (C) 2016 -- 2017, Sagemath Inc.
6
#
7
# This program is free software: you can redistribute it and/or modify
8
# it under the terms of the GNU General Public License as published by
9
# the Free Software Foundation, either version 3 of the License, or
10
# (at your option) any later version.
11
#
12
# This program is distributed in the hope that it will be useful,
13
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
16
#
17
# You should have received a copy of the GNU General Public License
18
# along with this program. If not, see <http://www.gnu.org/licenses/>.
19
#
20
###############################################################################
21
22
#########################################
23
# Sending emails
24
#########################################
25
26
BANNED_DOMAINS = {'qq.com':true}
27
28
29
fs = require('fs')
30
async = require('async')
31
winston = require('winston') # logging -- https://github.com/flatiron/winston
32
33
winston.remove(winston.transports.Console)
34
winston.add(winston.transports.Console, {level: 'debug', timestamp:true, colorize:true})
35
36
# sendgrid API: https://sendgrid.com/docs/API_Reference/Web_API/mail.html
37
sendgrid = require("sendgrid")
38
39
misc = require('smc-util/misc')
40
{defaults, required} = misc
41
42
{SENDGRID_TEMPLATE_ID, SENDGRID_ASM_NEWSLETTER, COMPANY_NAME, COMPANY_EMAIL} = require('smc-util/theme')
43
44
email_server = undefined
45
46
exports.is_banned = is_banned = (address) ->
47
i = address.indexOf('@')
48
if i == -1
49
return false
50
x = address.slice(i+1).toLowerCase()
51
return !! BANNED_DOMAINS[x]
52
53
# here's how I test this function:
54
# require('email').send_email(subject:'TEST MESSAGE', body:'body', to:'[email protected]', cb:console.log)
55
exports.send_email = send_email = (opts={}) ->
56
opts = defaults opts,
57
subject : required
58
body : required
59
fromname : COMPANY_NAME
60
from : COMPANY_EMAIL
61
to : required
62
replyto : undefined
63
replyto_name : undefined
64
cc : ''
65
bcc : ''
66
verbose : true
67
cb : undefined
68
category : undefined
69
asm_group : undefined
70
71
if opts.verbose
72
dbg = (m) -> winston.debug("send_email(to:#{opts.to}) -- #{m}")
73
else
74
dbg = (m) ->
75
dbg(opts.body)
76
77
if is_banned(opts.to) or is_banned(opts.from)
78
dbg("WARNING: attempt to send banned email")
79
opts.cb?('banned domain')
80
return
81
82
disabled = false
83
async.series([
84
(cb) ->
85
if email_server?
86
cb(); return
87
dbg("starting sendgrid client...")
88
filename = "#{process.env.SALVUS_ROOT}/data/secrets/sendgrid"
89
fs.readFile filename, 'utf8', (error, api_key) ->
90
if error
91
err = "unable to read the file '#{filename}', which is needed to send emails."
92
dbg(err)
93
cb(err)
94
else
95
api_key = api_key.toString().trim()
96
if api_key.length == 0
97
dbg("email_server: explicitly disabled -- so pretend to always succeed for testing purposes")
98
disabled = true
99
email_server = {disabled:true}
100
cb()
101
return
102
email_server = sendgrid(api_key)
103
dbg("started sendgrid client")
104
cb()
105
(cb) ->
106
if disabled or email_server?.disabled
107
cb(undefined, 'sendgrid email disabled -- no actual message sent')
108
return
109
dbg("sending email to #{opts.to} starting...")
110
# Sendgrid V3 API -- https://sendgrid.com/docs/API_Reference/Web_API_v3/Mail/index.html
111
helper = sendgrid.mail
112
from_email = new helper.Email(opts.from, opts.fromname)
113
to_email = new helper.Email(opts.to)
114
content = new helper.Content("text/html", opts.body)
115
mail = new helper.Mail(from_email, opts.subject, to_email, content)
116
if opts.replyto
117
replyto_name = opts.replyto_name ? opts.replyto
118
mail.setReplyTo(new helper.Email(opts.replyto, replyto_name))
119
120
personalization = new helper.Personalization()
121
personalization.setSubject(opts.subject)
122
personalization.addTo(to_email)
123
if opts.cc
124
personalization.addCc(new helper.Email(opts.cc))
125
if opts.bcc
126
personalization.addBcc(new helper.Email(opts.bcc))
127
128
# one or more strings to categorize the sent emails on sendgrid
129
if opts.category?
130
mail.addCategory(new helper.Category(opts.category))
131
132
# to unsubscribe only from a specific type of email, not everything!
133
# https://app.sendgrid.com/suppressions/advanced_suppression_manager
134
if opts.asm_group?
135
mail.setAsm(new helper.Asm(opts.asm_group))
136
137
# plain template with a header (smc logo), a h1 title, and a footer
138
mail.setTemplateId(SENDGRID_TEMPLATE_ID)
139
# This #title# will end up below the header in an <h1> according to the template
140
personalization.addSubstitution(new helper.Substitution("#title#", opts.subject))
141
142
mail.addPersonalization(personalization)
143
144
# Sendgrid V3 API
145
request = email_server.emptyRequest
146
method : 'POST'
147
path : '/v3/mail/send'
148
body : mail.toJSON()
149
150
email_server.API request, (err, res) ->
151
dbg("sending email to #{opts.to} done...; got err=#{misc.to_json(err)} and res=#{misc.to_json(res)}")
152
if err
153
dbg("sending email -- error = #{misc.to_json(err)}")
154
else
155
dbg("sending email -- success = #{misc.to_json(res)}")
156
cb(err)
157
], (err, message) ->
158
if err
159
# so next time it will try fresh to connect to email server, rather than being wrecked forever.
160
email_server = undefined
161
err = "error sending email -- #{misc.to_json(err)}"
162
dbg(err)
163
else
164
dbg("successfully sent email")
165
opts.cb?(err, message)
166
)
167
168
169
# Send a mass email to every address in a file.
170
# E.g., put the email addresses in a file named 'a' and
171
# require('email').mass_email(subject:'TEST MESSAGE', body:'body', to:'a', cb:console.log)
172
exports.mass_email = (opts) ->
173
opts = defaults opts,
174
subject : required
175
body : required
176
from : COMPANY_EMAIL
177
fromname : COMPANY_NAME
178
to : required # array or string (if string, opens and reads from file, splitting on whitspace)
179
cc : ''
180
limit : 10 # number to send in parallel
181
cb : undefined # cb(err, list of recipients that we succeeded in sending email to)
182
183
dbg = (m) -> winston.debug("mass_email: #{m}")
184
dbg(opts.filename)
185
dbg(opts.subject)
186
dbg(opts.body)
187
success = []
188
recipients = undefined
189
190
async.series([
191
(cb) ->
192
if typeof(opts.to) != 'string'
193
recipients = opts.to
194
cb()
195
else
196
fs.readFile opts.to, (err, data) ->
197
if err
198
cb(err)
199
else
200
recipients = misc.split(data.toString())
201
cb()
202
(cb) ->
203
n = 0
204
f = (to, cb) ->
205
if n % 100 == 0
206
dbg("#{n}/#{recipients.length-1}")
207
n += 1
208
send_email
209
subject : opts.subject
210
body : opts.body
211
from : opts.from
212
fromname : opts.fromname
213
to : to
214
cc : opts.cc
215
asm_group: SENDGRID_ASM_NEWSLETTER
216
category : "newsletter"
217
verbose : false
218
cb : (err) ->
219
if not err
220
success.push(to)
221
cb()
222
else
223
cb("error sending email to #{to} -- #{err}")
224
225
async.mapLimit(recipients, opts.limit, f, cb)
226
], (err) ->
227
opts.cb?(err, success)
228
)
229
230
231
232