<commit_before>""" IMAPClient wrapper for Inbox.

Unfortunately, due to IMAP's statefulness, to implement connection pooling we
have to shunt off dealing with the connection pool to the caller or we'll end
up trying to execute calls with the wrong folder selected some amount of the
time. That's why functions take a connection argument.
"""
import imaplib
import functools

from collections import namedtuple

import gevent
from gevent import socket

import geventconnpool

from inbox.util.concurrency import retry
from inbox.util.misc import or_none, timed
from inbox.basicauth import AUTH_TYPES
from inbox.models.session import session_scope
from inbox.models.backends.imap import ImapAccount
from inbox.log import get_logger
logger = get_logger()

__all__ = ['CrispinClient', 'GmailCrispinClient',
           'YahooCrispinClient', 'OutlookCrispinClient',
           'AOLCrispinClient']

# Unify flags API across IMAP and Gmail
Flags = namedtuple('Flags', 'flags')
# Flags includes labels on Gmail because Gmail doesn't use \Draft.
GmailFlags = namedtuple('GmailFlags', 'flags labels')

GMetadata = namedtuple('GMetadata', 'msgid thrid')
RawMessage = namedtuple(
    'RawImapMessage',
    'uid internaldate flags body g_thrid g_msgid g_labels created')


class GmailSettingError(Exception):
    """ Thrown on misconfigured Gmail accounts. """
    pass


def connection_pool(account_id, pool_size=6, connection_pool_for=dict()):
    """ Per-account crispin connection pool.

    Use like this:

        with crispin.connection_pool(account_id).get() as crispin_client:
            # your code here
            pass

    Note that the returned CrispinClient could have ANY folder selected, or
    none at all! It's up to the calling code to handle folder sessions
    properly. We don't reset to a certain select state because it's slow.
    """
    pool = connection_pool_for.get(account_id)
    if pool is None:
        pool = connection_pool_for[account_id] \
            = CrispinConnectionPool(account_id, num_connections=pool_size,
                                    readonly=True)
    return pool


def writable_connection_pool(account_id, pool_size=2,
                             connection_pool_for=dict()):
    """ Per-account crispin connection pool, with *read-write* connections.

    Use like this:

        conn_pool = crispin.writable_connection_pool(account_id)
        with conn_pool.get() as crispin_client:
            # your code here
            pass
    """
    pool = connection_pool_for.get(account_id)
    if pool is None:
        pool = connection_pool_for[account_id] \
            = CrispinConnectionPool(account_id, num_connections=pool_size,
                                    readonly=False)
    return pool

CONN_DISCARD_EXC_CLASSES = (socket.error, imaplib.IMAP4.error)


class CrispinConnectionPool(geventconnpool.ConnectionPool):
    """
    Connection pool for Crispin clients.

    Connections in a pool are specific to an IMAPAccount.

    Parameters
    ----------
    account_id : int
        Which IMAPAccount to open up a connection to.
    num_connections : int
        How many connections in the pool.
    readonly : bool
        Is the connection to the IMAP server read-only?<commit_msg>Prevent concurrent creation of duplicate connection pools.<commit_after>""" IMAPClient wrapper for Inbox.

Unfortunately, due to IMAP's statefulness, to implement connection pooling we
have to shunt off dealing with the connection pool to the caller or we'll end
up trying to execute calls with the wrong folder selected some amount of the
time. That's why functions take a connection argument.
"""
import imaplib
import functools
import threading

from collections import namedtuple, defaultdict

import gevent
from gevent import socket

import geventconnpool

from inbox.util.concurrency import retry
from inbox.util.misc import or_none, timed
from inbox.basicauth import AUTH_TYPES
from inbox.models.session import session_scope
from inbox.models.backends.imap import ImapAccount
from inbox.log import get_logger
logger = get_logger()

__all__ = ['CrispinClient', 'GmailCrispinClient',
           'YahooCrispinClient', 'OutlookCrispinClient',
           'AOLCrispinClient']

# Unify flags API across IMAP and Gmail
Flags = namedtuple('Flags', 'flags')
# Flags includes labels on Gmail because Gmail doesn't use \Draft.
GmailFlags = namedtuple('GmailFlags', 'flags labels')

GMetadata = namedtuple('GMetadata', 'msgid thrid')
RawMessage = namedtuple(
    'RawImapMessage',
    'uid internaldate flags body g_thrid g_msgid g_labels created')

# Lazily-initialized map of account ids to lock objects.
# This prevents multiple greenlets from concurrently creating duplicate
# connection pools for a given account.
_lock_map = defaultdict(threading.Lock)


class GmailSettingError(Exception):
    """ Thrown on misconfigured Gmail accounts. """
    pass


def connection_pool(account_id, pool_size=6, connection_pool_for=dict()):
    """ Per-account crispin connection pool.

    Use like this:

        with crispin.connection_pool(account_id).get() as crispin_client:
            # your code here
            pass

    Note that the returned CrispinClient could have ANY folder selected, or
    none at all! It's up to the calling code to handle folder sessions
    properly. We don't reset to a certain select state because it's slow.
    """
    with _lock_map[account_id]:
        pool = connection_pool_for.get(account_id)
        if pool is None:
            pool = connection_pool_for[account_id] \
                = CrispinConnectionPool(account_id, num_connections=pool_size,
                                        readonly=True)
        return pool


def writable_connection_pool(account_id, pool_size=2,
                             connection_pool_for=dict()):
    """ Per-account crispin connection pool, with *read-write* connections.

    Use like this:

        conn_pool = crispin.writable_connection_pool(account_id)
        with conn_pool.get() as crispin_client:
            # your code here
            pass
    """
    with _lock_map[account_id]:
        pool = connection_pool_for.get(account_id)
        if pool is None:
            pool = connection_pool_for[account_id] \
                = CrispinConnectionPool(account_id, num_connections=pool_size,
                                        readonly=False)
        return pool

CONN_DISCARD_EXC_CLASSES = (socket.error, imaplib.IMAP4.error)


class CrispinConnectionPool(geventconnpool.ConnectionPool):
    """
    Connection pool for Crispin clients.

    Connections in a pool are specific to an IMAPAccount.

    Parameters
    ----------
    account_id : int
        Which IMAPAccount to open up a connection to.
    num_connections : int
        How many connections in the pool.
    readonly : bool
        Is the connection to the IMAP server read-only?
{'fraction_non_alphanumeric': 0.06807272727272727, 'fraction_numerical': 0.0008727272727272727, 'mean_word_length': 3.162227602905569, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 3, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 4, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4070793', 'n_tokens_mistral': 1928, 'n_tokens_neox': 1857, 'n_words': 638}