Ejemplo n.º 1
0
class Misc(db.Model, BaseMixin):
    __tablename__ = 'misc'

    id = Column(db.Integer, primary_key=True)
    name = Column(db.String(STRING_LEN))
    data = Column(JSON)
    count = Column(db.Integer)
    created_at = Column(db.DateTime(timezone=True), default=db.func.now())
    modified_at = Column(db.DateTime(timezone=True),
                         default=db.func.now(),
                         onupdate=db.func.now())

    @classmethod
    def do_stuff(self):
        '''
        Used for load testing
        '''
        r = self.create(name='create_random',
                        data={
                            'dog': 'cat',
                            'black': 'white'
                        })
        r2 = Misc.find_by_id_anon(r.id)
        r2.update(data={})
        r2.update(data={'yes': 'no'})
        r2.save()
        r2.delete()
        r2 = Misc.find_by_id_anon(r.id)
        redis.incr('load_test')
Ejemplo n.º 2
0
class Template(db.Model, BaseMixin):
    __tablename__ = 'templates'

    id = Column(db.Integer, primary_key=True)
    display_name = Column(db.String(STRING_LEN))
    file_name = Column(db.String(STRING_LEN))
    html = Column(db.Text())
    url = Column(db.String(STRING_LEN))
    active = Column(db.Integer)
    order = Column(db.Integer)
    created_at = Column(db.DateTime(timezone=True), default=db.func.now())
    modified_at = Column(db.DateTime(timezone=True),
                         default=db.func.now(),
                         onupdate=db.func.now())

    @classmethod
    def get_all(cls):
        return [x for x in cls.query.order_by(cls.order.asc()).all()]

    @classmethod
    def get_all_choices(cls):
        return [(t.id, t.file_name, t.display_name) for t in cls.get_all()]

    @classmethod
    def get_html(cls, template_id):
        template = cls.query.filter_by(id=template_id).first()
        if template:
            path = os.path.abspath(
                os.path.join(current_app.config.get('PROJECT_ROOT'), 'static',
                             'templates', template.file_name + ".html"))
            with open(path) as myfile:
                html = myfile.read()
            return html
        else:
            return None
Ejemplo n.º 3
0
class Account(db.Model, BaseMixin):
    __tablename__ = 'accounts'

    id = Column(db.Integer, primary_key=True)
    name = Column(db.String(STRING_LEN), nullable=False, unique=True)
    domain = Column(db.String(STRING_LEN), nullable=False)
    subaccount_name = Column(db.String(STRING_LEN))
    api_key = Column(db.String(STRING_LEN))
    dedicated_ip = Column(db.String(STRING_LEN))
    active = Column(db.SmallInteger, default=1)
    default_from_email = Column(db.String(STRING_LEN))
    contact_email = Column(db.String(STRING_LEN))
    phone = Column(db.String(STRING_LEN))
    address = Column(db.String(STRING_LEN))
    city = Column(db.String(STRING_LEN))
    state = Column(db.String(STRING_LEN))
    zip_code = Column(db.String(STRING_LEN))
    country = Column(db.Integer)
    auto_text = Column(db.Integer)
    footer_html = Column(db.Text())
    footer_text = Column(db.Text())
    created_at = Column(db.DateTime(timezone=True),  default=db.func.now())
    modified_at = Column(db.DateTime(timezone=True), default=db.func.now(), onupdate=db.func.now())


    def ip_pool(self):
        return self.dedicated_ip or None


    @classmethod
    def find_by_name(cls, name):
        return cls.query.filter_by(name=name).first()
Ejemplo n.º 4
0
class User(db.Model, UserMixin, BaseMixin):
    __tablename__ = 'users'

    id = Column(db.Integer, primary_key=True)
    account_id = Column(db.Integer,
                        db.ForeignKey('accounts.id'),
                        nullable=False)
    account = relationship("Account",
                           primaryjoin="Account.id==User.account_id",
                           foreign_keys="User.account_id")
    email = Column(db.String(STRING_LEN), nullable=False, unique=True)
    first_name = Column(db.String(STRING_LEN), nullable=False)
    last_name = Column(db.String(STRING_LEN), nullable=False)
    accepted_terms = Column(db.SmallInteger, default=0)
    activation_key = Column(db.String(STRING_LEN))
    password = Column(db.String(STRING_LEN), nullable=False)
    phone = Column(db.String(STRING_LEN))
    image = Column(db.String(STRING_LEN))
    confirmed_at = Column(db.DateTime(timezone=True))
    login_count = Column(db.Integer)
    current_login_at = Column(db.DateTime(timezone=True))
    last_login_at = Column(db.DateTime(timezone=True))
    current_login_ip = Column(db.String(STRING_LEN))
    last_login_ip = Column(db.String(STRING_LEN))
    active = db.Column(db.Boolean(), default=True)
    roles = db.relationship('Role',
                            secondary=roles_users,
                            backref=db.backref('users', lazy='dynamic'))
    created_at = Column(db.DateTime(timezone=True), default=db.func.now())
    modified_at = Column(db.DateTime(timezone=True),
                         default=db.func.now(),
                         onupdate=db.func.now())

    def __repr__(self):
        return '<User: %s>' % self.email

    @property
    def name(self):
        return ' '.join(
            [self.first_name.capitalize(),
             self.last_name.capitalize()])

    @classmethod
    def get_by_id(cls, user_id):
        return cls.query.filter_by(id=user_id).first_or_404()
Ejemplo n.º 5
0
class Blacklist(db.Model, BaseMixin):
    __tablename__ = 'blacklists'

    id = Column(db.Integer, primary_key=True)
    account_id = Column(db.Integer,
                        db.ForeignKey('accounts.id'),
                        nullable=False)
    email = Column(db.String(STRING_LEN), index=True)
    detail = Column(db.String(STRING_LEN))
    reason = Column(db.String(STRING_LEN))
    manual = Column(db.Integer, default=0)
    created_at = Column(db.DateTime(timezone=True), default=db.func.now())

    @classmethod
    def check(cls, account_id, email):
        return cls.query.with_entities(cls.id).filter(
            cls.account_id == account_id, cls.email == email).count() or 0

    @classmethod
    def insert(cls, account_id, email, reason, detail):
        if not Blacklist.check(account_id, email):
            if detail:
                detail = (detail[:240] + '..') if len(detail) > 240 else detail
            if reason:
                reason = (reason[:240] + '..') if len(reason) > 240 else reason
            Blacklist.create(account_id=account_id,
                             email=email,
                             reason=reason,
                             detail=detail)
            return True
        else:
            return False

    @classmethod
    def save_file(cls, account_id, file):
        filename = secure_filename(file.filename)
        if current_app.debug:
            # Local Save
            account_path = os.path.join(current_app.config['IMPORT_FOLDER'],
                                        str(account_id))
            make_dir(account_path)
            blacklist_path = os.path.join(account_path, 'blacklists')
            make_dir(blacklist_path)
            file_path = os.path.join(blacklist_path, filename)
            file.save(file_path)
            return file_path
        else:
            # S3 Save
            return upload_blacklist(account_id, filename, file)
Ejemplo n.º 6
0
class Unsubscribe(db.Model, BaseMixin):
    __tablename__ = 'unsubscribes'

    id = Column(db.Integer, primary_key=True)
    account_id = Column(db.Integer,
                        db.ForeignKey('accounts.id'),
                        nullable=False)
    email = Column(db.String(STRING_LEN), index=True)
    manual = Column(db.Integer, default=0)
    created_at = Column(db.DateTime(timezone=True), default=db.func.now())

    @classmethod
    def check(cls, account_id, email):
        return cls.query.with_entities(cls.id).filter(
            cls.account_id == account_id, cls.email == email).count() or 0

    @classmethod
    def get_all_emails(cls):
        unsubscribes = cls.find_all().all()
        emails = []
        for unsub in unsubscribes:
            emails.append(unsub.email)
        return emails

    @classmethod
    def generate_csv(cls):
        content = StringIO.StringIO()
        cw = csv.writer(content)

        unsubscribes = cls.find_all().all()

        for unsub in unsubscribes:
            created = arrow.get(
                unsub.created_at).to('US/Arizona').format('MM/DD/YY HH:mm:ss')
            cw.writerow([unsub.email, created])

        ot = arrow.get(datetime.utcnow())
        ot = ot.to('US/Arizona').format('MM/DD/YY HH:mm:ss')
        filename = "unsubscribes-{}.csv".format(ot)
        response = make_response(content.getvalue())
        response.headers[
            "Content-Disposition"] = "attachment; filename=%s" % filename
        response.headers["Content-type"] = "text/csv"
        return response
Ejemplo n.º 7
0
class List(db.Model, BaseMixin):
    __tablename__ = 'lists'

    id = Column(db.Integer, primary_key=True)
    account_id = Column(db.Integer,
                        db.ForeignKey('accounts.id'),
                        nullable=False)
    name = Column(db.String(STRING_LEN), nullable=False)
    filename = Column(db.String(STRING_LEN))
    import_data = deferred(db.Column(JSON))
    created_at = Column(db.DateTime(timezone=True), default=db.func.now())
    modified_at = Column(db.DateTime(timezone=True),
                         default=db.func.now(),
                         onupdate=db.func.now())
    account = relationship("Account",
                           primaryjoin="Account.id==List.account_id",
                           foreign_keys="List.account_id")
    campaigns = relationship("Campaign",
                             primaryjoin="Campaign.list_id==List.id",
                             foreign_keys="Campaign.list_id")

    def __repr__(self):
        return '<List: %s - %s>' % (self.id, self.name)

    @classmethod
    def save_file(cls, file, list_id, curr_acct_id):
        if current_app.debug:
            # Local Save
            filename = secure_filename(file.filename)
            account_path = os.path.join(current_app.config['IMPORT_FOLDER'],
                                        str(curr_acct_id))
            make_dir(account_path)
            list_path = os.path.join(account_path, str(list_id))
            make_dir(list_path)
            file_path = os.path.join(list_path, filename)
            file.save(file_path)
            return file_path
        else:
            # S3 Save
            filename = secure_filename(file.filename)
            return upload_list(curr_acct_id, list_id, filename, file)

    def total_send_count(self):
        return len(self.import_data) if self.import_data else 0

    def get_import_data_keys(self):
        if self.import_data:
            keys = []
            for k, v in self.import_data[0].iteritems():
                if k != "hash_id":
                    keys.append(k)
            keys.sort()
            return keys
        else:
            return []

    def get_unique_col_values(self, col, tup=False):
        if self.import_data:
            vals = set([])
            for row in self.import_data:
                if row.get(col):
                    vals.add(row.get(col))
            if tup:
                cover = [('', '')]
                for v in vals:
                    cover.append((v, v))
                return cover
            else:
                return vals
        else:
            return []

    def random_data_sample(self, sample_size=15):
        if self.import_data:
            random_set = range(len(self.import_data))
            sample_size = sample_size if len(self.import_data) >= 15 else len(
                self.import_data)
            randos = random.sample(random_set, sample_size)
            random_data = []
            for r in randos:
                random_data.append(self.import_data[r])
            return random_data
        else:
            return None
Ejemplo n.º 8
0
class Dispatcher(db.Model, BaseMixin):
    __tablename__ = 'dispatches'

    id = Column(db.Integer, primary_key=True)
    state = Column(db.Integer, default=0)
    send_at = Column(db.DateTime(timezone=True))
    percent_complete = Column(db.Integer, default=0)
    import_data = deferred(db.Column(JSON))
    adjusted_data = deferred(db.Column(JSON))
    queued_count = Column(db.Integer, default=0)
    sent_count = Column(db.Integer, default=0)
    skipped_count = Column(db.Integer, default=0)
    account_id = Column(db.Integer,
                        db.ForeignKey('accounts.id'),
                        nullable=False)
    user_id = Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
    campaign_id = Column(db.Integer,
                         db.ForeignKey('campaigns.id'),
                         nullable=False)
    list_id = Column(db.Integer, db.ForeignKey('lists.id'), nullable=False)
    created_at = Column(db.DateTime(timezone=True), default=db.func.now())
    modified_at = Column(db.DateTime(timezone=True),
                         default=db.func.now(),
                         onupdate=db.func.now())

    account = relationship("Account",
                           primaryjoin="Account.id==Dispatcher.account_id",
                           foreign_keys="Dispatcher.account_id")
    user = relationship("User",
                        primaryjoin="User.id==Dispatcher.user_id",
                        foreign_keys="Dispatcher.user_id")
    list_ = relationship("List",
                         primaryjoin="List.id==Dispatcher.list_id",
                         foreign_keys="Dispatcher.list_id")
    campaign = relationship("Campaign",
                            primaryjoin="Campaign.id==Dispatcher.campaign_id",
                            foreign_keys="Dispatcher.campaign_id")
    sends = relationship("Send",
                         primaryjoin="Dispatcher.id==Send.dispatcher_id",
                         foreign_keys="Send.dispatcher_id")

    ###
    # State Machine
    #
    # 0. Pending - a dispatcher object is created
    # 1. Preparing data - run prep_data on dispatchers import_data
    # 2. After prep_data is finished it calls queue_emails which updates to stage 2 and
    #    creates the Send objects from the import_data. Is indempotent, can be run again.
    # 3. Sending Emails - stage 2 is done and so is dispatcher, now it moves it to stage 3 which is just
    #    the Send objects going out.
    # 4. We run a beat job to see if all sends are out, if they are we move to this 'complete' stage.
    #
    # Failures:
    #
    # 101: really means the import data failed in prep_data. Just call send() again to move forward, may need a data fix.
    # 102: somewhere in the middle of creating the Send objects we errored out, call send() which calls queue_emails again
    # Alt: After we are done we could call queue_emails again to double check all sends were created, and also
    #      beat Will run retry_mail() to retry any failed Send objects.
    #
    ###

    STATES = {
        0: 'Pending',
        1: 'Preparing Data',
        2: 'Queueing Emails',
        3: 'Sending Emails',
        10: 'Complete',
        15: 'Scheduled Send',
        100: 'Too soon for a resend, try again in a few minutes.',
        101: 'Failed to load data correctly. Please contact support.',
        102: 'Failed to queue all emails for sending. Please contact support.',
        103: 'Data not prepared correctly. Please contact support.',
        104: 'Invalid List Data.'
    }

    def current_state(self):
        if self.state == 15:
            dt = arrow.get(self.send_at)
            dt = dt.to('US/Arizona')
            return "%s %s" % (self.STATES.get(self.state), dt.humanize())
        else:
            return self.STATES.get(self.state) if self.STATES.get(
                self.state) else ''

    def send(self):
        if Dispatcher.check_for_recent(self.campaign_id, self.id):
            self.update(state=100)
        elif self.state == 0 or self.state == 15:
            prep_data_task.delay(self.id)
        else:
            current_app.logger.info(
                "\n send cannot send in current state dispatch: %s, state: %s \n"
                % (self.id, self.state))

    def next(self):
        if (self.state == 0):
            self.send()
            print 'send()'
            return 'send()'
        elif (self.state == 1):
            self.send()
            print 'send()'
            return 'send()'
        elif (self.state == 2):
            queue_emails_task.delay(self.id)
            print 'queue_emails()'
            return 'queue_emails()'
        elif (self.state == 3):
            print "Nothing to do"
            return 'Nothing to do'
        elif (self.state == 10):
            print "Complete, nothing to do"
            return 'Complete, Nothing to do'
        elif (self.state == 100):
            print "Nothing to do"
            return 'Nothing to do'
        elif (self.state == 101):
            self.send()
            print 'send()'
            return 'send()'
        elif (self.state == 102):
            self.expire_counts()
            queue_emails_task.delay(self.id)
            print 'queue_emails()'
            return 'queue_emails()'
        else:
            print "Bad state"
            return "Bad State"

    ###
    # 3 Primary State Machine Methods
    ###

    def prep_data(self):
        ''' Add a hash_id to every row. Thats it. '''
        self.update(state=1)
        new_data = []
        for index, customer in enumerate(self.import_data):
            if not customer.get('hash_id'):
                hash_id = os.urandom(32).encode('hex')
                while Send.find_by_hash(hash_id):
                    hash_id = os.urandom(32).encode('hex')
                customer['hash_id'] = hash_id
                new_data.append(customer)
        self.set_adjusted_data(new_data)
        queue_emails_task.delay(self.id)

    def queue_emails(self):
        ''' If first and last row have hash_id, create a task for each row, and incr_queued.
            Lastly, set the final queued_count and update state.
        '''
        self.update(state=2)
        try:
            data = self.adjusted_data
            if data and len(data) > 0 and type(data[0]) is dict:
                count = len(data) - 1
                if count >= 0 and (not data[0].get('hash_id')
                                   or not data[count].get('hash_id')):
                    self.update(
                        state=103
                    )  # if first or last rows don't have hash_id, don't send any
                else:
                    for customer_data in data:
                        send_email_task.delay(self.id, customer_data)
                        self.incr_queued()
                    self.state = 3
                    self.queued_count = self.get_queued()  # final tally
                    self.save()
            else:
                self.update(state=104)
        except:
            self.update(state=102)
            raise

    def send_email_from_data(self, data):
        ''' Given a row of data, make sure this hash_id doesn't exist, and has email.
            If so, create a send and process it.
        '''
        hash_id = data.get('hash_id')  # no dups, this way its idempotent
        send = Send.find_by_hash(hash_id)
        if send:
            # This is used for retry_for_lost_tasks and 102's. (Counts should have been expired.)
            # If half the sends are already sent then we mark those here, but we want send.process() to mark actual send.
            if send.attempts > 0:
                self.incr_sent(
                )  # else: it will handle itself below when calling process()
        else:
            email = self.campaign.email_determiner(data)
            if not email:
                self.incr_skipped()
            else:
                send = Send.create(email_id=email.id,
                                   dispatcher_id=self.id,
                                   account_id=self.account_id,
                                   data=data,
                                   hash_id=hash_id)
                if send:
                    # If there is an email and the send was created...
                    send.process()
                else:
                    current_app.logger.info(
                        "\n send_email_from_data: send failed to create: %s \n"
                        % hash_id)

    ###
    # Minor State Machine Methods
    ###

    def incr_queued(self):
        return redis.incr('dispatcher_%s_queued' % self.id)

    def get_queued(self):
        return redis.get('dispatcher_%s_queued' % self.id) or 0

    def incr_skipped(self):
        return redis.incr('dispatcher_%s_skipped' % self.id)

    def get_skipped(self):
        return redis.get('dispatcher_%s_skipped' % self.id) or 0

    def incr_sent(self):
        return redis.incr('dispatcher_%s_sent' % self.id)

    def get_sent(self):
        return redis.get('dispatcher_%s_sent' % self.id) or 0

    def expire_counts(self, timed=None):
        if timed:
            redis.expire('dispatcher_%s_percent' % self.id, timed)
            redis.expire('dispatcher_%s_queued' % self.id, timed)
            redis.expire('dispatcher_%s_sent' % self.id, timed)
            redis.expire('dispatcher_%s_skipped' % self.id, timed)
        else:
            redis.delete('dispatcher_%s_percent' % self.id)
            redis.delete('dispatcher_%s_queued' % self.id)
            redis.delete('dispatcher_%s_sent' % self.id)
            redis.delete('dispatcher_%s_skipped' % self.id)

    def get_percent_complete(self):
        '''
        This method calculates how many sends/queues exist and makes a percentage
        If percentage is 100 it saves the dispatch as completed
        If not its cached for 15 seconds
        '''
        if self.percent_complete == 100:
            return 100
        else:
            key = ('dispatcher_%s_percent' % self.id)
            if redis.exists(key):
                return int(redis.get(key))
            else:
                curr_sent_count = int(self.get_sent())
                queued = int(self.queued_count if self.queued_count else self.
                             get_queued())
                processed = curr_sent_count + int(self.get_skipped())
                percent = int(
                    (float(processed) / queued) * 100) if queued > 0 else 0

                # Note: self.queued_count is only set after queuing is done
                # percent needs to be >98 to allow old/dead jobs to run an hour or two later, data updates then too.
                if percent > 98 and self.queued_count:
                    self.sent_count = curr_sent_count
                    self.skipped_count = int(self.get_skipped())
                    self.percent_complete = 100
                    self.state = 10
                    self.save()
                    self.expire_counts(86400)
                else:
                    self.update(sent_count=curr_sent_count)
                    redis.set(key, percent)
                    redis.expire(
                        key, 15)  # cache current percent done for 15 seconds
                return percent

    ###
    # Other Helper Methods
    ###

    def set_adjusted_data(self, new_data):
        self.update(
            adjusted_data=[])  # fixes flask-sqlalchemy bug (ticket open)
        self.update(adjusted_data=new_data)

    @classmethod
    def check_for_recent(cls, campaign_id, dispatch_id=None):
        time_limit = datetime.utcnow() - timedelta(minutes=2)
        if dispatch_id:
            return cls.query.filter(
                and_(cls.campaign_id == campaign_id, cls.created_at >
                     time_limit, cls.id != dispatch_id)).count() > 0
        else:
            return cls.query.filter(
                and_(cls.campaign_id == campaign_id,
                     cls.created_at > time_limit)).count() > 0

    # If nothing in mail queue and percent < 100, maybe check unattempted.
    # Todo: automate this with beat
    def retry_for_lost_tasks(self):
        '''
        Note: Try retry_failures first before this.
        Situation:
            - Count is 100 queued, 88 sent, 2 skipped, and lets say 10 were lost silently
            - Now we are stuck at 88%, and _no_ jobs left in mail queue, no idea how many failed (but here its 10)
            - Or: redis goes down and we loose all counts
        Solution:
            - find all created sends, reset redis counts to that, then retry all and 10 lost tasks are queued
        '''
        if self.state == 3 or self.state == 10:
            already_sent_count = int(
                Send.query.filter_by(dispatcher_id=self.id).count())
            unattempted = int(
                Send.query.filter(
                    and_(Send.dispatcher_id == self.id,
                         Send.attempts == 0)).count())
            self.expire_counts()
            self.update(percent_complete=0)
            self.queue_emails()
            time.sleep(.5)
            new_get_queued = int(self.get_queued())
            result = "\n Already Sent: %s \n Unattempted: %s \n" % (
                already_sent_count, unattempted)
            print result
            return result
        else:
            return "Can't find lost tasks in current state"
Ejemplo n.º 9
0
class Email(db.Model, BaseMixin):
    __tablename__ = 'emails'

    id = Column(db.Integer, primary_key=True)
    account_id = Column(db.Integer,
                        db.ForeignKey('accounts.id'),
                        nullable=False)
    selector_col_val = Column(db.String(STRING_LEN))
    name = Column(db.String(STRING_LEN), nullable=False)
    subject = Column(db.String(STRING_LEN), nullable=True)
    preheader = Column(db.String(STRING_LEN))
    html = Column(db.Text())
    text = Column(db.Text())
    created_at = Column(db.DateTime(timezone=True), default=db.func.now())
    modified_at = Column(db.DateTime(timezone=True),
                         default=db.func.now(),
                         onupdate=db.func.now())
    account = relationship("Account",
                           primaryjoin="Account.id==Email.account_id",
                           foreign_keys="Email.account_id")
    campaign_id = Column(db.Integer,
                         db.ForeignKey('campaigns.id'),
                         nullable=False)
    campaign = relationship("Campaign",
                            primaryjoin="Campaign.id==Email.campaign_id",
                            foreign_keys="Email.campaign_id")
    sends = relationship("Send",
                         primaryjoin="Email.id==Send.email_id",
                         foreign_keys="Send.email_id")

    def full_html(self):
        footer = self.account.footer_html
        html = self.html
        if footer or self.preheader:
            soup = BeautifulSoup(html)
            if soup.body:
                if footer:
                    soup.body.append(BeautifulSoup(footer))
                if self.preheader:
                    preheader = "<span class='preheader'>%s</span>" % self.preheader
                    soup.body.insert(0, preheader)
                html = unicode(soup.html)
            else:
                if footer:
                    html = "%s<br>%s" % (html, footer)
                if self.preheader:
                    preheader = "<span class='preheader'>%s</span>" % self.preheader
                    html = preheader + html
        return urllib.unquote(html)

    def full_text(self):
        footer = self.account.footer_text
        text = self.text
        if footer:
            text = "%s\n\n%s" % (text, footer)
        return urllib.unquote(text)

    def render_html_attr(self, data, hash_id):
        data = Email.add_additional_data(data, hash_id)
        html = self.full_html()
        return pystache.render(html, data)

    def render_text_attr(self, data, hash_id):
        data = Email.add_additional_data(data, hash_id)
        text = self.full_text()
        return pystache.render(text, data)

    def check_keys(self, list_id):
        list_ = List.find_by_id_anon(list_id)
        keys = list_.random_data_sample(1)[0]
        renderer = pystache.Renderer(missing_tags='strict')
        try:
            renderer.render(self.html, keys)
            return True, None
        except KeyNotFoundError, e:
            return False, e.key
Ejemplo n.º 10
0
class Send(db.Model, BaseMixin):
    __tablename__ = 'sends'

    id = Column(db.Integer, primary_key=True)
    hash_id = Column(db.String, index=True, unique=True)
    email_id = Column(db.Integer, db.ForeignKey('emails.id'), nullable=False)
    account_id = Column(db.Integer, db.ForeignKey('accounts.id'))
    dispatcher_id = Column(db.Integer, db.ForeignKey('dispatches.id'))
    data = Column(JSON)  # from csv
    message = Column(JSON)  # dynamically generated api request sent to mandril
    result = Column(JSON)  #  result from mandril
    status = Column(db.String)
    state = Column(db.Integer, default=0)
    attempts = Column(db.Integer, default=0)
    created_at = Column(db.DateTime(timezone=True), default=db.func.now())
    modified_at = Column(db.DateTime(timezone=True),
                         default=db.func.now(),
                         onupdate=db.func.now())
    account = relationship("Account",
                           primaryjoin="Account.id==Send.account_id",
                           foreign_keys="Send.account_id")
    email = relationship("Email",
                         primaryjoin="Email.id==Send.email_id",
                         foreign_keys="Send.email_id")
    dispatcher = relationship("Dispatcher",
                              primaryjoin="Dispatcher.id==Send.dispatcher_id",
                              foreign_keys="Send.dispatcher_id")

    # Send result
    STATES = {
        0: 'unsent',
        1: 'success',
        2: 'error',
        3: 'rejected',
        4: 'invalid',
        5: 'queued',
        6: 'unsubscribed',
        7: 'blacklisted',
        8: 'bad to_email',
        9: 'unknown',
        103: 'no hash_id'
    }

    def current_state(self):
        return self.STATES.get(self.state) if self.STATES.get(
            self.state) else ''

    @classmethod
    def find_by_hash(cls, hash_id):
        return cls.query.filter(cls.hash_id == str(hash_id)).first()

    @classmethod
    def hash_exists(cls, hash_id):
        return cls.query.with_entities(
            cls.id).filter(cls.hash_id == hash_id).first()

    @classmethod
    def delete_old_sends(cls):
        # TODO, delete sends over 2 months old
        pass

    def process(self):
        ''' the actual send '''

        self.update(attempts=self.attempts + 1)
        if self.attempts == 1:
            self.dispatcher.incr_sent()

        if self.state == 0 or self.state == 2:
            try:
                if not self.hash_id:
                    self.update(state=103)
                else:
                    campaign = self.dispatcher.campaign
                    data = self.data
                    to_email = campaign.to_email_ov if campaign.to_email_ov else campaign.render_attr(
                        'to_email_dd', data)
                    if Unsubscribe.check(self.account_id, to_email):
                        self.update(state=6)  #they already unsubscribed
                    elif Blacklist.check(self.account_id, to_email):
                        self.update(state=7)  #they are blacklisted
                    elif not to_email or len(to_email) < 6:
                        self.update(state=8)
                    else:
                        email = self.email
                        from_email = campaign.from_email_ov if campaign.from_email_ov else campaign.render_attr(
                            'from_email_dd', data)
                        from_name = campaign.from_name_ov if campaign.from_name_ov else campaign.render_attr(
                            'from_name_dd', data)
                        auto_text = self.account.auto_text
                        message = {
                            "html":
                            email.render_html_attr(data, data['hash_id']),
                            "subject":
                            email.subject,
                            "from_email":
                            from_email,
                            "from_name":
                            from_name,
                            "to": [{
                                "email":
                                to_email,
                                "name":
                                campaign.to_name_ov if campaign.to_name_ov else
                                campaign.render_attr('to_name_dd', data),
                                "type":
                                "to"
                            }],
                            'headers': {
                                "Reply-To":
                                campaign.reply_to_ov if campaign.reply_to_ov
                                else campaign.render_attr('reply_to_dd', data)
                            },
                            'important':
                            False,
                            'track_opens':
                            True,
                            'track_clicks':
                            True,
                            "auto_text":
                            auto_text,
                            "auto_html":
                            False,
                            "inline_css":
                            False,
                            "url_strip_qs":
                            False,
                            "preserve_recipients":
                            False,
                            "view_content_link":
                            False,
                            "signing_domain":
                            self.account.domain,
                            "merge":
                            False,
                            "metadata": {
                                "hash_id": self.hash_id,
                                "dispatcher_id": self.dispatcher_id
                            },
                            "recipient_metadata": [{
                                "rcpt": to_email
                            }]
                        }
                        if not auto_text:
                            message['text'] = email.render_text_attr(
                                data, data['hash_id'])

                        if not current_app.debug:
                            mc = mandrill.Mandrill(self.account.api_key)
                            self.result = mc.messages.send(
                                message=message,
                                async=False,
                                ip_pool=self.account.ip_pool())
                            self.status = self.result[0].get('status') if (
                                self.result and self.result[0]) else None

                            if self.status == 'sent':
                                self.state = 1
                            elif self.status == 'rejected':
                                self.state = 3
                            elif self.status == 'invalid':
                                self.state = 4
                            elif self.status == 'queued' or self.status == 'scheduled':
                                self.state = 5
                            else:
                                self.state = 9
                        else:
                            self.state = 1  #fake success in debug mode

                        self.message = message
                        self.save()

            except mandrill.Error, e:
                self.update(state=2)
                # Mandrill errors are thrown as exceptions
                print "mandrill_error: %s - %s" % (e.__class__, e)
                raise
Ejemplo n.º 11
0
class Campaign(db.Model, BaseMixin):
    __tablename__ = 'campaigns'

    id = Column(db.Integer, primary_key=True)
    name = Column(db.String(STRING_LEN), nullable=False)
    account_id = Column(db.Integer,
                        db.ForeignKey('accounts.id'),
                        nullable=False)
    selector_col_name = Column(db.String(STRING_LEN))
    from_email_dd = Column(db.String(STRING_LEN))  # dd = dynamic data
    from_name_dd = Column(db.String(STRING_LEN))
    reply_to_dd = Column(db.String(STRING_LEN))
    to_email_dd = Column(db.String(STRING_LEN))
    to_name_dd = Column(db.String(STRING_LEN))
    from_email_ov = Column(db.String(STRING_LEN))  # ov = overwrite
    from_name_ov = Column(db.String(STRING_LEN))
    reply_to_ov = Column(db.String(STRING_LEN))
    to_email_ov = Column(db.String(STRING_LEN))
    to_name_ov = Column(db.String(STRING_LEN))
    created_at = Column(db.DateTime(timezone=True), default=db.func.now())
    modified_at = Column(db.DateTime(timezone=True),
                         default=db.func.now(),
                         onupdate=db.func.now())
    account = relationship("Account",
                           primaryjoin="Account.id==Campaign.account_id",
                           foreign_keys="Campaign.account_id")
    list_id = Column(db.Integer)
    list_ = relationship("List",
                         primaryjoin="List.id==Campaign.list_id",
                         foreign_keys="Campaign.list_id")
    emails = relationship("Email",
                          primaryjoin="Campaign.id==Email.campaign_id",
                          foreign_keys="Email.campaign_id")
    dispatcher = relationship(
        "Dispatcher",
        primaryjoin="Campaign.id==Dispatcher.campaign_id",
        foreign_keys="Dispatcher.campaign_id")

    def __repr__(self):
        return '<Campaign: %s - %s>' % (self.id, self.name)

    def check_email_keys(self):
        for email in self.emails:
            valid, bad_key = email.check_keys(self.list_id)
            if not valid:
                return False, bad_key
        return True, None

    def render_attr(self, attr, data):
        return pystache.render("{{ %s }}" % getattr(self, attr), data)

    def email_determiner(self, data):
        ''' Find which of the campaign's email's this data runs with
            returns first one found
        '''
        if self.selector_col_name and len(self.selector_col_name) > 0:
            col_name_val = data.get(self.selector_col_name)
            if col_name_val and len(col_name_val) > 0:
                for email in self.emails:
                    if email.selector_col_val:
                        vals = json.loads(email.selector_col_val)
                        for val in vals:
                            if val and len(val) > 0 and col_name_val == val:
                                return email
        else:
            return self.emails[0] if len(self.emails) else None

    def get_selector_import_data(self):
        selector_data = []
        for data in self.list_.import_data:
            if self.email_determiner(data):
                selector_data.append(data)
        return selector_data

    def selector_send_count(self):
        key = 'selector_send_count_%s' % self.id
        val = get_cache(key)
        if val:
            return val
        else:
            count = 0
            for data in self.list_.import_data:
                if self.email_determiner(data):
                    count = count + 1
            set_cache(key, count, 10)
            return count

    def determiner_duplicates(self):
        ''' Used so one person doesn't get multiple emails.
            Returns (True, val) if dups or (false, None) if its ok.
        '''
        values = []
        for email in self.emails:
            if email.selector_col_val:
                vals = json.loads(email.selector_col_val)
                for val in vals:
                    if val and val in values:
                        return True, val
                    else:
                        values.append(val)
        return False, None

    def selector_missing(self):
        if len(self.emails) > 1 and not self.selector_col_name:
            return True
        return False