Beispiel #1
0
def get_candidates_from_smartlist(list_id,
                                  candidate_ids_only=False,
                                  user_id=None):
    """
    Calls inter services method and retrieves the candidates of a smart or dumb list.
    :param list_id: smartlist id.
    :param candidate_ids_only: Whether or not to get only ids of candidates
    :param user_id: Id of user.
    :type list_id: int | long
    :type candidate_ids_only: bool
    :type user_id: int | long | None
    :rtype: list
    """
    raise_if_not_positive_int_or_long(list_id)
    raise_if_not_instance_of(candidate_ids_only, bool)
    raise_if_not_positive_int_or_long(user_id)
    candidates = []
    try:
        candidates = get_candidates_of_smartlist(
            list_id=list_id,
            candidate_ids_only=candidate_ids_only,
            access_token=None,
            user_id=user_id)
    except Exception as error:
        logger.error(
            'Error occurred while getting candidates of smartlist(id:%s).'
            '.Reason: %s' % (list_id, error.message))
    logger.info("There are %s candidates in smartlist(id:%s)" %
                (len(candidates), list_id))
    return candidates
Beispiel #2
0
    def patch(self, campaign_id):
        """
        This endpoint updates an existing campaign
        :param int|long campaign_id: Id of campaign
        """
        raise_if_dict_values_are_not_int_or_long(dict(campaign_id=campaign_id))
        # Get and validate request data
        data = request.get_json(silent=True)
        if not data:
            raise InvalidUsage("Received empty request body",
                               error_code=INVALID_REQUEST_BODY[1])
        is_hidden = data.get('is_hidden', False)
        if is_hidden not in (True, False, 1, 0):
            raise InvalidUsage(
                "is_hidden field should be a boolean, given: %s" % is_hidden,
                error_code=INVALID_INPUT[1])
        email_campaign = EmailCampaignBase.get_campaign_if_domain_is_valid(
            campaign_id, request.user)
        # Unschedule task from scheduler_service
        if email_campaign.scheduler_task_id:
            headers = {'Authorization': request.oauth_token}
            # campaign was scheduled, remove task from scheduler_service
            if CampaignUtils.delete_scheduled_task(
                    email_campaign.scheduler_task_id, headers):
                email_campaign.update(
                    scheduler_task_id='')  # Delete scheduler task id

        email_campaign.update(is_hidden=is_hidden)
        logger.info("Email campaign(id:%s) has been archived successfully" %
                    campaign_id)
        return dict(message="Email campaign (id: %s) updated successfully" %
                    campaign_id), codes.OK
Beispiel #3
0
def do_mergetag_replacements(texts,
                             current_user,
                             requested_object=None,
                             candidate_address=None):
    """
    Here we do the replacements of merge tags with required values. This serves for candidate and user.
    If no candidate or user is provided, name is set to "John Doe".
    It replaces MergeTags with candidate's or user's first name, last name.
    It also replaces preferences URL only for candidate.
    :param list[> 0](string) texts: List of e.g. subject, body_text and body_html
    :rtype: list[> 0](string)
    """
    if not isinstance(current_user, User):
        raise InternalServerError('Invalid object passed for user')
    if requested_object and not isinstance(requested_object,
                                           (Candidate, User)):
        raise InternalServerError('Invalid object passed')

    first_name = "John"
    last_name = "Doe"

    if requested_object:
        first_name = requested_object.first_name if requested_object.first_name else first_name
        last_name = requested_object.last_name if requested_object.last_name else last_name

    new_texts = []
    merge_tag_replacement_dict = {
        DEFAULT_FIRST_NAME_MERGETAG: first_name,
        DEFAULT_LAST_NAME_MERGETAG: last_name,
        DEFAULT_USER_NAME_MERGETAG: current_user.name
    }
    logger.info('Replacing merge tags for campaign of user(id:%s, name:%s)' %
                (current_user.id, current_user.name))
    for text in texts:
        if text:
            logger.info('Replacing merge tags for string:%s' % text)
            for key, value in merge_tag_replacement_dict.iteritems():
                if key in text:
                    # Do first_name, last_name and username replacements
                    logger.info('Merge tag:%s found and replaced with:%s' %
                                (key, value))
                    text = text.replace(key, value)
                else:
                    logger.info('Merge tag:%s was not found. Value:%s' %
                                (key, value))
            # Do 'Unsubscribe' link replacements
            if isinstance(
                    requested_object,
                    Candidate) and DEFAULT_PREFERENCES_URL_MERGETAG in text:
                text = do_prefs_url_replacement(text, requested_object,
                                                candidate_address)
            elif isinstance(requested_object,
                            User) and DEFAULT_PREFERENCES_URL_MERGETAG in text:
                text = text.replace(DEFAULT_PREFERENCES_URL_MERGETAG,
                                    TEST_PREFERENCE_URL)

        new_texts.append(text)
    logger.info('Converted body_html, body_text and subject are:%s' %
                new_texts)
    return new_texts
Beispiel #4
0
    def post(self):
        """
        This will save an entry in database table email_client_credentials.

        .. Request Body::
                        {
                            "host": "Host Name",
                            "port": 123,
                            "name": "Server Name",
                            "email": "*****@*****.**",
                            "password": "******",
                        }

        .. Response::
                       {
                          "id": 347
                       }

        .. Status:: 201 (Resource created)
                    400 (Bad request)
                    401 (Unauthorized to access getTalent)
                    500 (Internal server error)
        """
        data = get_json_data_if_validated(request, EMAIL_CLIENTS_SCHEMA)
        data = format_email_client_data(data)
        data['user_id'] = request.user.id
        client_in_db = EmailClientCredentials.get_by_user_id_host_and_email(
            data['user_id'], data['host'], data['email'])
        if client_in_db:
            raise InvalidUsage(
                'Email client with given data already present in database')

        client = EmailClientBase.get_client(data['host'])
        client = client(data['host'], data['port'], data['email'],
                        data['password'])
        logger.info('Connecting with given email-client')
        client.connect()
        client.authenticate()
        logger.info(
            'Successfully connected and authenticated with given email-client')
        # Encrypt password
        ciphered_password = encrypt(
            app.config[TalentConfigKeys.ENCRYPTION_KEY], data['password'])
        b64_password = b64encode(ciphered_password)
        data['password'] = b64_password
        email_client = EmailClientCredentials(**data)
        EmailClientCredentials.save(email_client)
        headers = {
            'Location':
            EmailCampaignApiUrl.EMAIL_CLIENT_WITH_ID % email_client.id
        }
        return ApiResponse(dict(id=email_client.id),
                           status=requests.codes.CREATED,
                           headers=headers)
Beispiel #5
0
    def post(self, campaign_id):
        """
        Sends campaign emails to the candidates present in smartlists of campaign.
        Scheduler service will call this to send emails to candidates.
        :param int|long campaign_id: Campaign id
        """
        raise_if_dict_values_are_not_int_or_long(dict(campaign_id=campaign_id))
        email_campaign = EmailCampaignBase.get_campaign_if_domain_is_valid(
            campaign_id, request.user)
        if email_campaign.is_hidden:
            logger.info(
                "Email campaign(id:%s) is archived, it cannot be sent." %
                campaign_id)
            # Unschedule task from scheduler_service
            if email_campaign.scheduler_task_id:
                headers = {'Authorization': request.oauth_token}
                # campaign was scheduled, remove task from scheduler_service
                if CampaignUtils.delete_scheduled_task(
                        email_campaign.scheduler_task_id, headers):
                    email_campaign.update(
                        scheduler_task_id='')  # Delete scheduler task id
            raise ResourceNotFound("Email campaign(id:%s) has been deleted." %
                                   campaign_id,
                                   error_code=EMAIL_CAMPAIGN_NOT_FOUND[1])
        email_client_id = email_campaign.email_client_id
        results_send = send_email_campaign(request.user,
                                           email_campaign,
                                           new_candidates_only=False)
        if email_client_id:
            if not isinstance(results_send, list):
                raise InternalServerError(
                    error_message="Something went wrong, response is not list")
            data = {
                'email_campaign_sends': [{
                    'email_campaign_id':
                    email_campaign.id,
                    'new_html':
                    new_email_html_or_text.get('new_html'),
                    'new_text':
                    new_email_html_or_text.get('new_text'),
                    'candidate_email_address':
                    new_email_html_or_text.get('email')
                } for new_email_html_or_text in results_send]
            }
            return jsonify(data)

        return dict(
            message='email_campaign(id:%s) is being sent to candidates.' %
            campaign_id), codes.OK
Beispiel #6
0
 def send_email(self, to_address, subject, body):
     """
     This connects and authenticate with SMTP server and sends email to given email-address
     :param string to_address: Recipient's email address
     :param string subject: Subject of email
     :param string body: Body of email
     """
     self.connect()
     self.authenticate(connection_quit=False)
     msg = "From: %s\r\nTo: %s\r\nSubject: %s\n%s\n" % (
         self.email, to_address, subject, body)
     self.connection.sendmail(self.email, to_address, msg)
     logger.info('Email has been sent from:%s, to:%s via SMTP server.' %
                 (self.email, to_address))
     self.connection.quit()
Beispiel #7
0
def schedule_job_for_email_conversations():
    """
    Schedule general job that hits /v1/email-conversations endpoint every hour.
    """
    url = EmailCampaignApiUrl.EMAIL_CONVERSATIONS
    task_name = 'get_email_conversations'
    start_datetime = datetime.utcnow() + timedelta(seconds=15)
    # Schedule for next 100 years
    end_datetime = datetime.utcnow() + timedelta(weeks=52 * 100)
    frequency = 3600
    access_token = User.generate_jw_token()
    headers = {
        'Content-Type': 'application/json',
        'Authorization': access_token
    }
    data = {
        'start_datetime':
        start_datetime.strftime(DatetimeUtils.ISO8601_FORMAT),
        'end_datetime': end_datetime.strftime(DatetimeUtils.ISO8601_FORMAT),
        'frequency': frequency,
        'is_jwt_request': True
    }

    logger.info('Checking if `{}` task already running...'.format(task_name))
    response = requests.get(SchedulerApiUrl.TASK_NAME % task_name,
                            headers=headers)
    # If job is not scheduled then schedule it
    if response.status_code == requests.codes.NOT_FOUND:
        logger.info('Task `{}` not scheduled. Scheduling `{}` task.'.format(
            task_name, task_name))
        data.update({'url': url})
        data.update({'task_name': task_name, 'task_type': 'periodic'})
        response = requests.post(SchedulerApiUrl.TASKS,
                                 headers=headers,
                                 data=json.dumps(data))
        if response.status_code == codes.CREATED:
            logger.info('Task `{}` has been scheduled.'.format(task_name))
        elif response.json()['error']['code'] == TASK_ALREADY_SCHEDULED:
            logger.info('Job already scheduled. `{}`'.format(task_name))
        else:
            logger.error(response.text)
            raise InternalServerError(
                error_message=
                'Unable to schedule job for getting email-conversations')
    elif response.status_code == requests.codes.ok:
        logger.info('Job already scheduled. `{}`'.format(response.text))
    else:
        logger.error(response.text)
Beispiel #8
0
 def import_emails(self, candidate_id, candidate_email):
     """
     This will import emails of user's account to getTalent database table email-conversations.
     :param positive candidate_id: Id of candidate
     :param string candidate_email: Email of candidate
     """
     search_criteria = '(FROM "%s")' % candidate_email
     typ, [searched_data] = self.connection.search(None, search_criteria)
     msg_ids = [msg_id for msg_id in searched_data.split()]
     logger.info("Account:%s, %s email(s) found from %s." %
                 (self.email, len(msg_ids), candidate_email))
     for num in msg_ids:
         body = ''
         typ, data = self.connection.fetch(num, '(RFC822)')
         raw_email = data[0][1]
         raw_email_string = raw_email.decode('utf-8')
         # converts byte literal to string removing b''
         email_message = email.message_from_string(raw_email_string)
         for header in ['subject', 'to', 'from', 'date']:
             logger.info('%s: %s' % (header.title(), email_message[header]))
         # convert string date to datetime object
         email_received_datetime = parser.parse(email_message['date'])
         # this will loop through all the available multiparts in mail
         for part in email_message.walk():
             if part.get_content_type(
             ) == "text/plain":  # ignore attachments/html
                 body = part.get_payload(decode=True)
                 logger.info('Body: %s' % body)
         self.save_email_conversation(candidate_id,
                                      email_message['subject'].strip(),
                                      body.strip(), email_received_datetime)
Beispiel #9
0
def import_email_conversations(queue_name):
    """
    This gets all the records for incoming clients from database table email_client_credentials.
    It then calls "import_email_conversations_per_account" to imports email-conversations for selected email-client.
    :type queue_name: string
    """
    email_client_credentials = \
        EmailClientCredentials.get_by_client_type(EmailClientCredentials.CLIENT_TYPES['incoming'])
    if not email_client_credentials:
        logger.info('No IMAP/POP email-client found in database')
    for email_client_credential in email_client_credentials:
        logger.info(
            'Importing email-conversations from host:%s, account:%s, user_id:%s'
            % (email_client_credential.host, email_client_credential.email,
               email_client_credential.user.id))
        import_email_conversations_per_client.apply_async(
            [
                email_client_credential.host, email_client_credential.port,
                email_client_credential.email,
                email_client_credential.password,
                email_client_credential.user.id, email_client_credential.id
            ],
            queue_name=queue_name)
Beispiel #10
0
def create_email_campaign_url_conversion(destination_url,
                                         email_campaign_send_id,
                                         type_,
                                         destination_url_custom_params=None):
    """
    Creates url_conversion in DB and returns source url
    """

    # Insert url_conversion
    if destination_url_custom_params:
        destination_url = set_query_parameters(destination_url,
                                               destination_url_custom_params)

    url_conversion = UrlConversion(destination_url=destination_url,
                                   source_url='')
    UrlConversion.save(url_conversion)

    # source_url = current.HOST_NAME + str(URL(a='web', c='default', f='url_redirect',
    # args=url_conversion_id, hmac_key=current.HMAC_KEY))
    logger.info('create_email_campaign_url_conversion: url_conversion_id:%s' %
                url_conversion.id)
    signed_source_url = CampaignUtils.sign_redirect_url(
        EmailCampaignApiUrl.URL_REDIRECT % url_conversion.id,
        datetime.utcnow() + relativedelta(years=+1))

    # In case of prod, do not save source URL
    if CampaignUtils.IS_DEV:
        # Update source url
        url_conversion.update(source_url=signed_source_url)
    # Insert email_campaign_send_url_conversion
    email_campaign_send_url_conversion = EmailCampaignSendUrlConversion(
        email_campaign_send_id=email_campaign_send_id,
        url_conversion_id=url_conversion.id,
        type=type_)
    EmailCampaignSendUrlConversion.save(email_campaign_send_url_conversion)
    return signed_source_url
Beispiel #11
0
 def email_conversation_importer(self):
     """
     This imports email-conversations from the candidates of user.
     """
     logger.info('POP email-conversations importer needs to be implemented')
Beispiel #12
0
def amazon_sns_endpoint():
    """
    This endpoint handles email bounces and complaints using Amazon Simple Email Service (SES)
     and Simple Notification Service (SNS).

     To get email bounces and complaints callback hits, we have to setup two SNS topics,
     one for email bounces `email_bounces` and other one for email complains `email_complaints`.
     We have subscribed this HTTP endpoint (/amazon_sns_endpoint) for any notifications on `email_bounces` topic.
     When an email is bounced or someone complains about email, SES sends a JSON message with email
     information and cause of failure to our subscribed topics on SNS service which, in turn, sends an HTTP request to
     our subscribed endpoint with JSON message that can be processed to handle bounces and complaints.

     When a bounce occurs, we mark that email address as bounced and no further emails will be sent to this
     email address.

     Here is a wiki article explaining how to setup this.
     https://github.com/gettalent/talent-flask-services/wiki/Email-Bounces
    """
    data = json.loads(request.data)
    headers = request.headers

    logger.info('SNS Callback: Headers: %s\nRequest Data: %s', headers, data)

    # SNS first sends a confirmation request to this endpoint, we then confirm our subscription by sending a
    # GET request to given url in subscription request body.
    if request.headers.get(aws.HEADER_KEY) == aws.SUBSCRIBE:
        response = requests.get(data[aws.SUBSCRIBE_URL])
        if data[aws.TOPIC_ARN] not in response.text:
            logger.info(
                'Could not verify topic subscription. TopicArn: %s, RequestData: %s',
                data[aws.TOPIC_ARN], request.data)
            return 'Not verified', requests.codes.INTERNAL_SERVER_ERROR

        logger.info(
            'Aws SNS topic subscription for email notifications was successful.'
            '\nTopicArn: %s\nRequestData: %s', data[aws.TOPIC_ARN],
            request.data)

    elif request.headers.get(aws.HEADER_KEY) == aws.NOTIFICATION:
        # In case of notification, check its type (bounce or complaint) and process accordingly.
        data = json.loads(request.data)
        message = data[aws.MESSAGE]
        message = json.loads(message)
        message_id = message[aws.MAIL][aws.MESSAGE_ID]
        if message[aws.NOTIFICATION_TYPE] == aws.BOUNCE_NOTIFICATION:
            bounce = message[aws.BOUNCE]
            emails = [
                recipient[aws.EMAIL_ADDRESSES]
                for recipient in bounce[aws.BOUNCE_RECIPIENTS]
            ]
            handle_email_bounce(message_id, bounce, emails)

        elif message[aws.NOTIFICATION_TYPE] == aws.COMPLAINT_NOTIFICATION:
            pass  # TODO: Add implementation for complaints

    elif request.headers.get(aws.HEADER_KEY) == aws.UNSUBSCRIBE:
        logger.info(
            'SNS notifications for email campaign has been unsubscribed.'
            '\nRequestData: %s', request.data)
    else:
        logger.info('Invalid request. Request data %s', request.data)

    return 'Thanks SNS for notification'
Beispiel #13
0
"""
    Run Celery Worker

For Celery to run from command line, script runs as separate process with celery command

 Usage: open terminal cd to talent-flask-services directory

 Run the following command to start celery worker:

    $ celery -A email_campaign_service.email_campaign_app.celery_app worker --concurrency=4 --loglevel=info
"""

# Service Specific
from email_campaign_service.common.talent_celery import CELERY_WORKER_ARGS
from email_campaign_service.email_campaign_app import celery_app, logger, app
from email_campaign_service.common.talent_config_manager import TalentConfigKeys
from email_campaign_service.common.campaign_services.campaign_utils import CampaignUtils

try:
    logger.info("Starting Celery worker for:%s" % app.name)
    celery_app.start(argv=CELERY_WORKER_ARGS + [CampaignUtils.EMAIL] +
                     ['-n', CampaignUtils.EMAIL])
except Exception as e:
    logger.exception(
        "Couldn't start Celery worker for email_campaign_service in "
        "%s environment." % (app.config[TalentConfigKeys.ENV_KEY]))
Beispiel #14
0
def get_priority_emails(user, candidate_ids):
    """
    This returns tuple (candidate_id, email) choosing priority email form all the emails of candidate.
    :type user: User
    :type candidate_ids: list
    :rtype: list[tuple]
    """
    # Get candidate emails sorted by updated time and then by candidate_id
    candidate_email_rows = CandidateEmail.get_emails_by_updated_time_candidate_id_desc(
        candidate_ids)

    # list of tuples (candidate id, email address)
    group_id_and_email_and_labels = []

    # ids_and_email_and_labels will be [(1, '*****@*****.**', 1), (2, '*****@*****.**', 3), ...]
    # id_email_label: (id, email, label)
    ids_and_email_and_labels = [(row.candidate_id, row.address,
                                 row.email_label_id)
                                for row in candidate_email_rows]

    # Again sorting on the basis of candidate_id
    ids_and_email_and_labels = sorted(ids_and_email_and_labels,
                                      key=itemgetter(0))
    """
    After running groupby clause, the data will look like
    group_id_and_email_and_labels = [[(candidate_id1, email_address1, email_label1),
        (candidate_id2, email_address2, email_label2)],... ]
    """

    for key, group_id_email_label in itertools.groupby(
            ids_and_email_and_labels,
            lambda id_email_label: id_email_label[0]):
        group_id_and_email_and_labels.append(list(group_id_email_label))
    filtered_email_rows = []

    # Check if primary EmailLabel exist in db
    if not EmailLabel.get_primary_label_description(
    ) == EmailLabel.PRIMARY_DESCRIPTION:
        raise InternalServerError(
            "get_email_campaign_candidate_ids_and_emails: Email label with primary description not found in db."
        )

    # We don't know email_label id of primary email. So, get that from db
    email_label_id_desc_tuples = [(email_label.id, email_label.description)
                                  for email_label in EmailLabel.query.all()]

    # If there are multiple emails of a single candidate, then get the primary email if it exist, otherwise get any
    # other email
    for id_and_email_and_label in group_id_and_email_and_labels:
        _id, email = get_candidate_id_email_by_priority(
            id_and_email_and_label, email_label_id_desc_tuples)
        search_result = CandidateEmail.search_email_in_user_domain(
            User, user, email)
        if CandidateEmail.is_bounced_email(email):
            logger.info(
                'Skipping this email because this email address is marked as bounced.'
                'CandidateId : %s, Email: %s.' % (_id, email))
            continue
        # If there is only one candidate for an email-address in user's domain, we are good to go,
        # otherwise log error and send campaign email to that email id only once.
        if len(search_result) == 1:
            filtered_email_rows.append((_id, email))
        else:
            # Check if this email is already present in list of addresses to which campaign would be sent.
            # If so, omit the entry and continue.
            if any(email in emails for emails in filtered_email_rows):
                continue
            else:
                logger.error(
                    '%s candidates found for email address %s in user(id:%s)`s domain(id:%s). '
                    'Candidate ids are: %s' %
                    (len(search_result), email, user.id, user.domain_id, [
                        candidate_email.candidate_id
                        for candidate_email in search_result
                    ]))
                filtered_email_rows.append((_id, email))

    return filtered_email_rows
Beispiel #15
0
def create_email_campaign_url_conversions(new_html, new_text,
                                          is_track_text_clicks,
                                          is_track_html_clicks,
                                          custom_url_params_json,
                                          is_email_open_tracking, custom_html,
                                          email_campaign_send_id):
    soup = None

    # HTML open tracking
    logger.info(
        'create_email_campaign_url_conversions: email_campaign_send_id: %s' %
        email_campaign_send_id)
    if new_html and is_email_open_tracking:
        soup = BeautifulSoup(new_html, "lxml")
        num_conversions = convert_html_tag_attributes(
            soup,
            lambda url: create_email_campaign_url_conversion(
                url, email_campaign_send_id, TRACKING_URL_TYPE),
            tag="img",
            attribute="src",
            convert_first_only=True)

        # If no images found, add a tracking pixel
        if not num_conversions:
            image_url = TRACKING_PIXEL_URL
            new_image_url = create_email_campaign_url_conversion(
                image_url, email_campaign_send_id, TRACKING_URL_TYPE)
            new_image_tag = soup.new_tag("img", src=new_image_url)
            soup.insert(0, new_image_tag)

    # HTML click tracking
    if new_html and is_track_html_clicks:
        soup = soup or BeautifulSoup(new_html)

        # Fetch the custom URL params dict, if any
        if custom_url_params_json:

            destination_url_custom_params = json.loads(custom_url_params_json)
        else:
            destination_url_custom_params = dict()

        # Convert all of soup's <a href=> attributes

        convert_html_tag_attributes(
            soup,
            lambda url: create_email_campaign_url_conversion(
                url, email_campaign_send_id, HTML_CLICK_URL_TYPE,
                destination_url_custom_params),
            tag="a",
            attribute="href")
    # Add custom HTML. Doesn't technically belong in this function, but since we have access to the BeautifulSoup
    # object, let's do it here.
    if new_html and custom_html:
        soup = soup or BeautifulSoup(new_html)
        body_tag = soup.find(name="body") or soup.find(name="html")
        """
        :type: Tag | None
        """
        if body_tag:
            custom_html_soup = BeautifulSoup(custom_html)
            body_tag.insert(0, custom_html_soup)
        else:
            logger.error(
                "Email campaign HTML did not have a body or html tag, "
                "so couldn't insert custom_html! email_campaign_send_id=%s",
                email_campaign_send_id)

    # Convert soup object into new HTML
    if new_html and soup:
        new_html = soup.prettify()
        new_html = HTMLParser.HTMLParser().unescape(new_html)

    return new_text, new_html