Пример #1
0
 def security_send_mail(subject, recipient, template, **context):
     # this may be called from view which doesn't reference interest
     # if so pick up user's first interest to get from_email address
     if not g.interest:
         g.interest = context['user'].interests[0].interest if context['user'].interests else None
     if g.interest:
         from_email = localinterest().from_email
     # use default if user didn't have any interests
     else:
         from_email = current_app.config['SECURITY_EMAIL_SENDER']
         # copied from flask_security.utils.send_mail
         if isinstance(from_email, LocalProxy):
             from_email = from_email._get_current_object()
     ctx = ('security/email', template)
     html = render_template('%s/%s.html' % ctx, **context)
     text = render_template('%s/%s.txt' % ctx, **context)
     sendmail(subject, from_email, recipient, html=html, text=text)
Пример #2
0
def send_discuss_email(meeting_id):
    """
    send email to meeting invitees

    :param meeting_id: id of meeting
    :param subject: subject for message
    :param message: message in html format
    :return: list of addresses email was sent to
    """
    invites = Invite.query.filter_by(meeting_id=meeting_id).all()
    meeting = Meeting.query.filter_by(id=meeting_id).one()

    tolist = ['{} <{}>'.format(i.user.name, i.user.email) for i in invites]

    # use from address configured for email
    email = Email.query.filter_by(meeting_id=meeting_id,
                                  type=MEETING_INVITE_EMAIL,
                                  interest=localinterest()).one()
    fromaddr = email.from_email
    subject = email.subject
    message = email.message

    # create and send email
    context = {
        'meeting': meeting,
        'message': message,
        'meeting_text': meeting.meetingtype.meetingwording,
        'statusreport_text': meeting.meetingtype.statusreportwording,
        'invitation_text': meeting.meetingtype.invitewording,
        'aninvitation_text':
        inflect_engine.a(meeting.meetingtype.invitewording),
    }
    for meetingoption in MEETING_OPTIONS:
        context[meetingoption] = meeting_has_option(meeting, meetingoption)

    html = render_template('meeting-discuss-email.jinja2', **context)

    sendmail(subject, fromaddr, tolist, html)

    return tolist
Пример #3
0
def send_evote_req(motion, localuser, from_addr, subject, message):
    """
    send user evote request

    :param motion: Motion instance
    :param localuser: LocalUser instance
    :param from_addr: from address for email
    :param subject: subject for email
    :param message: message for email
    :return: MotionVote instance
    """
    evote = MotionVote.query.filter_by(interest=localinterest(),
                                       motion=motion,
                                       user=localuser).one_or_none()
    if not evote:
        raise ParameterError(
            'motion vote for motion {}, user {} not found'.format(
                motion.id, localuser.email))

    evoteurl = page_url_for(
        'admin.motionvote',
        interest=g.interest,
        urlargs={MOTIONVOTE_KEY_URLARG: evote.motionvotekey},
        _external=True)

    context = {
        'motion': motion,
        'meeting': motion.meeting,
        'evoteurl': evoteurl,
        'message': message,
    }

    html = render_template('motion-evote-email.jinja2', **context)
    tolist = localuser.email
    cclist = None
    sendmail(subject, from_addr, tolist, html, ccaddr=cclist)

    return evote
Пример #4
0
def main():
    parser = ArgumentParser()
    parser.add_argument('interest')
    parser.add_argument('--nomembers',
                        default=False,
                        action='store_const',
                        const=True,
                        help='use --nomembers to skip members')
    parser.add_argument('--nomanagers',
                        default=False,
                        action='store_const',
                        const=True,
                        help='use --nomanagers to skip managers')
    args = parser.parse_args()

    scriptdir = dirname(__file__)
    # two levels up
    scriptfolder = dirname(dirname(scriptdir))
    configdir = join(scriptfolder, 'config')
    memberconfigfile = "members.cfg"
    memberconfigpath = join(configdir, memberconfigfile)
    userconfigfile = "users.cfg"
    userconfigpath = join(configdir, userconfigfile)

    # create app and get configuration
    # use this order so members.cfg overrrides users.cfg
    configfiles = [userconfigpath, memberconfigpath]
    app = create_app(Development(configfiles), configfiles)

    # set up database
    db.init_app(app)

    # set up app and request contexts so that taskdetails works
    # https://flask.palletsprojects.com/en/1.1.x/testing/
    with app.app_context(), app.test_request_context():
        # turn on logging
        setlogging()

        # g has to be set within app context
        g.interest = args.interest

        membertasks = []
        taskdetails.open()
        for row in taskdetails.rows:
            membertask = taskdetails.dte.get_response_data(row)

            # add user record
            localuserid, taskid = taskdetails.getids(row.id)
            membertask['User'] = localuser2user(localuserid)
            membertask['Task'] = Task.query.filter_by(id=taskid).one()
            membertasks.append(membertask)

        # create member based data structure
        mem2tasks = {}
        for membertask in membertasks:
            mem2tasks.setdefault(membertask['User'].email, {'tasks': []})
            mem2tasks[membertask['User'].email]['tasks'].append(membertask)

        for member in mem2tasks:
            mem2tasks[member]['tasks'].sort(key=lambda t: t['order'])

        # default is from interest, may be overridden below, based on emailtemplate configuration
        fromlist = localinterest().from_email

        # allows for debugging of each section separately
        if not args.nomembers:
            emailtemplate = EmailTemplate.query.filter_by(
                templatename='member-email', interest=localinterest()).one()
            template = Template(emailtemplate.template)
            subject = emailtemplate.subject
            if emailtemplate.from_email:
                fromlist = emailtemplate.from_email
            refurl = url_for('admin.taskchecklist',
                             interest=g.interest,
                             _external=True)

            for emailaddr in mem2tasks:
                # only send email if there are tasks overdue or upcoming
                sendforstatus = [STATUS_EXPIRES_SOON, STATUS_OVERDUE]
                if len([
                        t for t in mem2tasks[emailaddr]['tasks']
                        if t['status'] in sendforstatus
                ]) > 0:
                    html = template.render(**mem2tasks[emailaddr],
                                           statuses=sendforstatus,
                                           refurl=refurl)
                    tolist = emailaddr
                    cclist = None
                    sendmail(subject, fromlist, tolist, html, ccaddr=cclist)

        # allows for debugging of each section separately
        if not args.nomanagers:
            # what groups are each member a part of?
            member2groups = {}
            for memberlocal in LocalUser.query.filter_by(
                    active=True, interest=localinterest()).all():
                memberglobal = localuser2user(memberlocal)
                member2groups[memberglobal.email] = {
                    'worker': memberlocal,
                    'taskgroups': set()
                }
                # drill down to get all taskgroups the member is responsible for
                for position in positions_active(memberlocal, date.today()):
                    get_position_taskgroups(
                        position,
                        member2groups[memberglobal.email]['taskgroups'])
                for taskgroup in memberlocal.taskgroups:
                    get_taskgroup_taskgroups(
                        taskgroup,
                        member2groups[memberglobal.email]['taskgroups'])

            # get list of responsible managers
            responsibility = {}
            positions = Position.query.filter_by(
                interest=localinterest()).all()
            for position in positions:
                positiontasks = set()
                positionworkers = set()
                theseemailgroups = set(position.emailgroups)
                for workeremail in member2groups:
                    # add worker if the worker's taskgroups intersect with these email groups
                    if theseemailgroups & member2groups[workeremail][
                            'taskgroups']:
                        positionworkers |= {
                            member2groups[workeremail]['worker']
                        }
                for emailgroup in position.emailgroups:
                    get_taskgroup_tasks(emailgroup, positiontasks)
                # only set responsibility if this position has management for some groups
                if position.emailgroups:
                    for manager in members_active(position, date.today()):
                        manageruser = localuser2user(manager)
                        responsibility.setdefault(manageruser.email, {
                            'tasks': set(),
                            'workers': set()
                        })
                        responsibility[
                            manageruser.email]['tasks'] |= positiontasks
                        responsibility[
                            manageruser.email]['workers'] |= positionworkers

            # set up template engine
            emailtemplate = EmailTemplate.query.filter_by(
                templatename='leader-email', interest=localinterest()).one()
            template = Template(emailtemplate.template)
            subject = emailtemplate.subject
            if emailtemplate.from_email:
                fromlist = emailtemplate.from_email
            refurl = url_for('admin.taskdetails',
                             interest=g.interest,
                             _external=True)

            # loop through responsible managers, setting up their email
            for manager in responsibility:
                manager2members = {'members': []}
                # need to convert to ids which are given by taskdetails
                for positionworker in responsibility[manager]['workers']:
                    resptasks = [
                        taskdetails.setid(positionworker.id, t.id)
                        for t in responsibility[manager]['tasks']
                    ]
                    positionuser = localuser2user(positionworker)
                    thesetasks = [
                        t for t in mem2tasks[positionuser.email]['tasks']
                        if t['rowid'] in resptasks
                        and t['status'] in [STATUS_OVERDUE]
                    ]
                    if thesetasks:
                        manager2members['members'].append({
                            'name':
                            positionuser.name,
                            'tasks':
                            thesetasks
                        })

                # only send if something to send
                if manager2members['members']:
                    html = template.render(**manager2members, refurl=refurl)
                    tolist = manager
                    cclist = None

                    sendmail(subject, fromlist, tolist, html, ccaddr=cclist)
Пример #5
0
def check_add_invite(meeting, localuser, agendaitem, sendemail=True):
    """
    check if user invite needs to be added

    :param meeting: Meeting instance
    :param localuser: LocalUser instance
    :param agendaitem: AgendaItem instance for invite to be attached to
    :param sendemail: True means send email to localuser
    :return: invite (may have been created)
    """
    invite = Invite.query.filter_by(interest=localinterest(),
                                    meeting=meeting,
                                    user=localuser).one_or_none()
    if not invite:
        # create unique key for invite - uuid4 gives unique key
        invitekey = uuid4().hex
        invite = Invite(
            interest=localinterest(),
            meeting=meeting,
            user=localuser,
            agendaitem=agendaitem,
            invitekey=invitekey,
            activeinvite=True,
            lastreminder=datetime.now(),
        )
        db.session.add(invite)
        db.session.flush()

        # optionally send email to user
        if sendemail:
            # get user's outstanding action items
            actionitems = ActionItem.query.filter_by(interest=localinterest(), assignee=localuser). \
                filter(ActionItem.status != ACTION_STATUS_CLOSED).all()

            email = Email.query.filter_by(meeting=meeting,
                                          type=MEETING_INVITE_EMAIL).one()
            subject = email.subject

            fromlist = email.from_email

            rsvpurl = page_url_for('admin.memberstatusreport',
                                   interest=g.interest,
                                   urlargs={INVITE_KEY_URLARG: invitekey},
                                   _external=True)
            actionitemurl = page_url_for('admin.myactionitems',
                                         interest=g.interest,
                                         _external=True)

            context = {
                'meeting':
                meeting,
                'actionitems':
                actionitems,
                'rsvpurl':
                rsvpurl,
                'actionitemurl':
                actionitemurl,
                'message':
                email.message,
                'meeting_text':
                meeting.meetingtype.meetingwording,
                'statusreport_text':
                meeting.meetingtype.statusreportwording,
                'invitation_text':
                meeting.meetingtype.invitewording,
                'aninvitation_text':
                inflect_engine.a(meeting.meetingtype.invitewording)
            }
            for meetingoption in MEETING_OPTIONS:
                context[meetingoption] = meeting_has_option(
                    meeting, meetingoption)

            html = render_template('meeting-invite-email.jinja2', **context)
            tolist = localuser.email
            cclist = None
            sendmail(subject, fromlist, tolist, html, ccaddr=cclist)

    invite.activeinvite = True
    return invite
Пример #6
0
def generatereminder(meetingid, member, positions):
    """
    generate a meeting reminder email to the user

    :param meetingid: id of meeting
    :param member: member to remind
    :param positions: positions for which this reminder is about
    :return: False if new invite sent, True if reminder sent
    """
    # find member's invitation, if it exists
    invite = Invite.query.filter_by(meeting_id=meetingid,
                                    user=member).one_or_none()
    meeting = Meeting.query.filter_by(id=meetingid).one()

    # invite already exists, send reminder
    if invite:
        # email record should exist, else software error, so it's ok to use one()
        email = Email.query.filter_by(interest=localinterest(),
                                      meeting_id=meetingid,
                                      type=MEETING_REMINDER_EMAIL).one()

        # send reminder email to user
        subject = email.subject
        fromlist = email.from_email
        message = email.message
        tolist = member.email
        cclist = None
        # options = email.options

        # get user's outstanding action items
        actionitems = ActionItem.query.filter_by(interest=localinterest(), assignee=member). \
            filter(ActionItem.status != ACTION_STATUS_CLOSED).all()

        # set up urls for email
        rsvpurl = page_url_for('admin.memberstatusreport',
                               interest=g.interest,
                               urlargs={INVITE_KEY_URLARG: invite.invitekey},
                               _external=True)
        actionitemurl = page_url_for('admin.myactionitems',
                                     interest=g.interest,
                                     _external=True)

        # filter positions to those which affect this member
        active_positions = positions_active(member, invite.meeting.date)
        memberpositions = [p for p in positions if p in active_positions]

        # create and send email
        context = {
            'meeting':
            invite.meeting,
            'message':
            message,
            'actionitems':
            actionitems,
            'rsvpurl':
            rsvpurl,
            'actionitemurl':
            actionitemurl,
            'meeting_text':
            invite.meeting.meetingtype.meetingwording,
            'statusreport_text':
            invite.meeting.meetingtype.statusreportwording,
            'invitation_text':
            invite.meeting.meetingtype.invitewording,
            'aninvitation_text':
            inflect_engine.a(invite.meeting.meetingtype.invitewording),
            'positions':
            memberpositions,
        }
        for meetingoption in MEETING_OPTIONS:
            context[meetingoption] = meeting_has_option(
                invite.meeting, meetingoption)

        html = render_template('meeting-reminder-email.jinja2', **context)

        sendmail(subject, fromlist, tolist, html, ccaddr=cclist)
        invite.lastreminder = datetime.now()
        reminder = True

    # invite doesn't exist yet, create and send invite
    else:
        meeting = Meeting.query.filter_by(id=meetingid).one()
        anyinvite = Invite.query.filter_by(interest=localinterest(),
                                           meeting=meeting).first()
        check_add_invite(meeting, member, anyinvite.agendaitem)
        reminder = False

    return reminder
Пример #7
0
    def post(self):
        try:
            val = DteFormValidate(ApplnValidator(allow_extra_fields=True))
            results = val.validate(request.form)
            if results['results']:
                raise ParameterError(results['results'])

            formdata = results['python']
            name = formdata['name']
            config = RacingTeamConfig.query.filter_by(
                **localinterest_query_params()).one_or_none()
            interest = Interest.query.filter_by(interest=g.interest).one()
            if not config:
                raise ParameterError(
                    'interest configuration needs to be created')

            # we're logging this now
            logtime = datetime.now()
            applnrec = RacingTeamApplication(interest=localinterest(),
                                             logtime=logtime)
            applnformfields = 'name,email,dob,gender,applntype,comments'.split(
                ',')
            applndbfields = 'name,email,dateofbirth,gender,type,comments'.split(
                ',')
            applnmapping = dict(zip(applndbfields, applnformfields))
            form2appln = Transform(applnmapping, sourceattr=False)
            form2appln.transform(request.form, applnrec)
            db.session.add(applnrec)

            # race result information
            mailfields = OrderedDict([
                ('name', 'Name'),
                ('email', 'Email'),
                ('dob', 'Birth Date'),
                ('gender', 'Gender'),
                ('race1_name', 'Race 1 - Name'),
                ('race1_location', 'Race 1 - Location'),
                ('race1_date', 'Race 1 - Date'),
                ('race1_age', 'Race 1 - Age'),
                ('race1_distance', 'Race 1 - Distance'),
                ('race1_units', ''),
                ('race1_time', 'Race 1 - Official Time (hh:mm:ss)'),
                ('race1_resultslink', 'Race 1 - Results Website'),
                ('race1_agegrade', 'Race 1 - Age Grade'),
                ('race2_name', 'Race 2 - Name'),
                ('race2_location', 'Race 2 - Location'),
                ('race2_date', 'Race 2 - Date'),
                ('race2_age', 'Race 2 - Age'),
                ('race2_distance', 'Race 2 - Distance'),
                ('race2_units', ''),
                ('race2_time', 'Race 2 - Official Time (hh:mm:ss)'),
                ('race2_resultslink', 'Race 2 - Results Website'),
                ('race2_agegrade', 'Race 2 - Age Grade'),
                ('comments', 'Comments'),
            ])

            for ndx in [1, 2]:
                resultsrec = RacingTeamResult(interest=localinterest())
                resultsform = f'race{ndx}_name,race{ndx}_date,race{ndx}_location,race{ndx}_resultslink,race{ndx}_distance,race{ndx}_units,race{ndx}_time,race{ndx}_agegrade,race{ndx}_age'.split(
                    ',')
                resultsdb = 'eventname,eventdate,location,url,distance,units,time,agegrade,age'.split(
                    ',')
                resultsmapping = dict(zip(resultsdb, resultsform))
                resultsxform = Transform(resultsmapping, sourceattr=False)
                resultsxform.transform(request.form, resultsrec)
                resultsrec.application = applnrec
                db.session.add(resultsrec)

            # commit database changes
            db.session.commit()

            # send confirmation email
            subject = f"[racing-team-application] New racing team application from {name}"
            body = div()
            with body:
                p('The following application for the racing team was submitted. If this is correct, '
                  f'no action is required. If you have any changes, please contact {config.fromemail}'
                  )
                with table(), tbody():
                    for field in mailfields:
                        with tr():
                            td(mailfields[field])
                            td(request.form[field])
                with p():
                    text(f'Racing Team - {config.fromemail}')
                    br()
                    text(f'{interest.description}')

            html = body.render()
            tolist = formdata['email']
            fromlist = config.fromemail
            cclist = config.applnccemail
            sendmail(subject, fromlist, tolist, html, ccaddr=cclist)

            return jsonify({'status': 'OK'})

        except Exception as e:
            db.session.rollback()
            exc_type, exc_value, exc_traceback = exc_info()
            current_app.logger.error(''.join(format_tb(exc_traceback)))
            error = format_exc()
            return jsonify({'status': 'error', 'error': escape(repr(e))})
Пример #8
0
    def post(self):
        try:
            val = DteFormValidate(InfoValidator(allow_extra_fields=True))
            results = val.validate(request.form)
            if results['results']:
                raise ParameterError(results['results'])

            formdata = results['python']
            name = formdata['common_name']
            localuser = LocalUser.query.filter_by(
                name=name, active=True, **localinterest_query_params()).one()
            member = RacingTeamMember.query.filter_by(
                localuser=localuser, **localinterest_query_params()).one()
            config = RacingTeamConfig.query.filter_by(
                **localinterest_query_params()).one_or_none()
            interest = Interest.query.filter_by(interest=g.interest).one()
            if not config:
                raise ParameterError(
                    'interest configuration needs to be created')

            # we're logging this now
            logtime = datetime.now()
            inforec = RacingTeamInfo(interest=localinterest(),
                                     member=member,
                                     logtime=logtime)
            db.session.add(inforec)

            # race result information
            if formdata['common_infotype'] == 'raceresult':
                mailfields = OrderedDict([
                    ('common_name', 'Name'),
                    ('common_eventname', 'Event Name'),
                    ('common_eventdate', 'Event Date'),
                    ('common_infotype', 'Submission Type'),
                    ('raceresult_distance', 'Distance'),
                    ('raceresult_units', ''),
                    ('raceresult_time', 'Official Time (hh:mm:ss)'),
                    ('raceresult_age', 'Age (Race Date)'),
                    ('raceresult_agegrade', 'Age Grade'),
                    ('raceresult_awards', 'Awards'),
                ])

                resultfields = {
                    'common_eventname': 'eventname',
                    'raceresult_distance': 'distance',
                    'raceresult_units': 'units',
                    'raceresult_time': 'time',
                    'raceresult_age': 'age',
                    'raceresult_agegrade': 'agegrade',
                    'raceresult_awards': 'awards',
                }

                resultsrec = RacingTeamResult(interest=localinterest())
                for field in resultfields:
                    setattr(resultsrec, resultfields[field],
                            request.form[field])
                # use conversion to datetime
                resultsrec.eventdate = formdata['common_eventdate']
                resultsrec.info = inforec
                db.session.add(resultsrec)

            else:
                mailfields = OrderedDict([
                    ('common_name', 'Name'),
                    ('common_eventname', 'Event Name'),
                    ('common_eventdate', 'Event Date'),
                    ('common_infotype', 'Submission Type'),
                    ('volunteer_hours', 'How Many Hours'),
                    ('volunteer_comments', 'Comments'),
                ])

                resultfields = {
                    'common_eventname': 'eventname',
                    'volunteer_hours': 'hours',
                    'volunteer_comments': 'comment',
                }

                volunteerrec = RacingTeamVolunteer(interest=localinterest())
                for field in resultfields:
                    setattr(volunteerrec, resultfields[field],
                            request.form[field])
                # use conversion to datetime
                volunteerrec.eventdate = formdata['common_eventdate']
                volunteerrec.info = inforec
                db.session.add(volunteerrec)

            # commit database changes
            db.session.commit()

            # send confirmation email
            subject = "[racing-team-info] New racing team information from {}".format(
                name)
            body = div()
            with body:
                p('The following information for the racing team was submitted. If this is correct, '
                  f'no action is required. If you have any changes, please contact {config.fromemail}'
                  )
                with table(), tbody():
                    for field in mailfields:
                        with tr():
                            td(mailfields[field])
                            td(request.form[field])
                with p():
                    text(f'Racing Team - {config.fromemail}')
                    br()
                    text(f'{interest.description}')

            html = body.render()
            tolist = member.localuser.email
            fromlist = config.fromemail
            cclist = config.infoccemail
            sendmail(subject, fromlist, tolist, html, ccaddr=cclist)

            return jsonify({'status': 'OK'})

        except Exception as e:
            db.session.rollback()
            exc_type, exc_value, exc_traceback = exc_info()
            current_app.logger.error(''.join(format_tb(exc_traceback)))
            error = format_exc()
            return jsonify({'status': 'error', 'error': escape(repr(e))})
Пример #9
0
    def post(self):
        # see https://flask.palletsprojects.com/en/1.1.x/patterns/fileuploads/
        # check if the post request has the file part
        try:
            if 'file[0]' not in request.files:
                return jsonify({
                    'status': 'error',
                    'error': 'No files submitted'
                })
            gs = FsrcGoogleAuthService(
                current_app.config['GSUITE_SERVICE_KEY_FILE'],
                current_app.config['GSUITE_SCOPES'])
            parentid = current_app.config['FSRC_SCHOLARSHIP_FOLDER']

            tmpdir = TemporaryDirectory(prefix='mbr-frsc-')
            for i in range(int(request.form['numfiles'])):
                file = request.files['file[{}]'.format(i)]
                # if user does not select file, browser also
                # submit an empty part without filename
                if file.filename == '':
                    return jsonify({
                        'status':
                        'error',
                        'error':
                        'Empty filename detected for file {}'.format(i)
                    })
                name = request.form.get('name', '')
                if not name:
                    return jsonify({
                        'status': 'error',
                        'error': 'Name must be supplied'
                    })
                email = request.form.get('email', '')
                if not email:
                    return jsonify({
                        'status': 'error',
                        'error': 'Email must be supplied'
                    })
                if file and allowed_file(file.filename):
                    filename = secure_filename(file.filename)
                    applnfoldername = '{} {}'.format(name, email)

                    # has applicant already submitted? reuse filedid if so
                    # should not be more than one of these, but use the first one if any found
                    applnfolders = gs.list_files(parentid,
                                                 filename=applnfoldername)
                    if applnfolders:
                        folderid = applnfolders[0][applnfoldername]
                    else:
                        folderid = gs.create_folder(parentid, applnfoldername)

                    current_app.logger.info(
                        'fsrcmemscholarshipappl: {}/{} processing '
                        'file {}'.format(name, email, filename))
                    docpath = pathjoin(tmpdir.name, filename)
                    file.save(docpath)
                    fileid = gs.create_file(folderid,
                                            filename,
                                            docpath,
                                            doctype=None)

            # remove temporary directory
            rmtree(tmpdir.name, ignore_errors=True)

            # send mail to administrator
            foldermeta = gs.drive.files().get(fileId=folderid,
                                              fields='webViewLink').execute()
            folderlink = foldermeta['webViewLink']
            subject = "[FSRC Memorial Scholarship] Application from {}".format(
                name)
            from dominate.tags import div, p, a
            from dominate.util import text
            body = div()
            with body:
                p('Application received from {} {}'.format(name, email))
                with p():
                    text('See ')
                    a(folderlink, href=folderlink)
            html = body.render()
            tolist = current_app.config['FSRC_SCHOLARSHIP_EMAIL']
            fromlist = current_app.config['FSRC_SCHOLARSHIP_EMAIL']
            cclist = None
            sendmail(subject, fromlist, tolist, html, ccaddr=cclist)

            return jsonify({'status': 'OK'})

        except Exception as e:
            from traceback import format_exc
            from html import escape
            error = format_exc()
            return jsonify({'status': 'error', 'error': escape(repr(e))})