diff options
Diffstat (limited to 'kmail/Mainpage.dox')
-rw-r--r-- | kmail/Mainpage.dox | 871 |
1 files changed, 871 insertions, 0 deletions
diff --git a/kmail/Mainpage.dox b/kmail/Mainpage.dox new file mode 100644 index 000000000..0b15ebfbc --- /dev/null +++ b/kmail/Mainpage.dox @@ -0,0 +1,871 @@ +/** \mainpage KMail architectural overview + +\section KMail design principles + +This file is intended to guide the reader's way through the KMail +codebase. It should esp. be handy for people not hacking full-time on +KMail as well as people that want to trace bugs in parts of KMail +which they don't know well. + +Contents: +- Kernel +- Identity +- Filters +- ConfigureDialog +- MDNs +- Folders +- Index +- Headers +- Display + +TODO: reader, composer, messages, accounts, ... + +\section kernel KERNEL + +Files: kmkernel.h, kmkernel.cpp + +Contact Zack Rusin <zack@kde.org> with questions... + +The first thing you'll notice about KMail is the extensive use of +kmkernel->xxx() constructs. The "kmkernel" is a define in kmkernel.h +declared as : +#define kmkernel KMKernel::self() +KMKernel is the central object in KMail. It's always created before +any other class, therefore you are _guaranteed_ that KMKernel::self() +(and therefore "kmkernel" construct) won't return 0 (null). + +KMKernel implements the KMailIface (our DCOP interface) and gives +access to all the core KMail functionality. + + +\section identity IDENTITY + +FIXME this has moved to libkpimidentities, right? + +Files: identity*, kmidentity.{h,cpp}, configuredialog.cpp, + signatureconfigurator.{h,cpp} + +Contact Marc Mutz <mutz@kde.org> on questions... + +Identities consists of various fields represented by +QStrings. Currently, those fields are hardcoded, but feel free to +implement KPIM::Identity as a map from strings to QVariants or somesuch. + +One part of identities are signatures. They can represent four modes +(Signature::Type) of operation (disabled, text from file or command +and inline text), which correspond to the combo box in the +identitydialog. + +Identities are designed to be used through the KPIM::IdentityManager: +const KPIM::Identity & ident = + kmkernel->identityManager()->identityForUoidOrDefault(...) +Make sure you assign to a _const_ reference, since the identityForFoo +methods are overloaded with non-const methods that access a different +list of identities in the manager that is used while configuring. That +is known source of errors when you use identityForFoo() as a parameter +to a method taking const KPIM::Identity &. + +WARNING: Don't hold the reference longer than the current functions +scope or next return to the event loop. That's b/c the config dialog +is not modal and the user may hit apply/ok anytime between calls to +function that want to use the identity reference. Store the UOID +instead if you need to keep track of the identity. You may also want +to connect to one of the KPIM::IdentityManager::changed() or ::deleted() +signals, if you want to do special processing in case the identity +changes. + +Thus, in the ConfigureDialog, you will see non-const KPIM::Identity +references being used, while everywhere else (KMMessage, +IdentityCombo) const references are used. + +The KPIM::IdentityCombo is what you see in the composer. It's a +self-updating combo box of KPIM::Identity's. Use this if you want the user +to choose an identity, e.g. in the folder dialog. + +Ihe IdentityListView is what you see in the config dialog's identity +management page. It's not meant to be used elsewhere, but is DnD +enabled (well, at the time of this writing, only drag-enabled). This +is going to be used to dnd identities around between KNode and KMail, +e.g. + +The SignatureConfigurator is the third tab in the identity +dialog. It's separate since it is used by the identity manager to +request a new file/command if the current value somehow fails. + + + +\section filter FILTER + +Contact Marc Mutz <mutz@kde.org> on questions... + +Filters consist of a search pattern and a list of actions plus a few +flags to indicate when they are to be applied (kmfilter.h). + They are managed in a QPtrList<KMFilter>, called KMFilterMgr. This +filter magnager is responsible for loading and storing filters +(read/writeConfig) and for executing them (process). The unique +instance of the filter manager is held by the kernel +(KMKernel::filterMgr()). + +The search pattern is a QPtrList of search rules (kmsearchpattern.h) and a +boolean operator that defines their relation (and/or). + +A search rule consists of a field-QString, a "function"-enum and a +"contents" or "value" QString. The first gives the header (or +pseudoheader) to match against, the second says how to match (equals, +consists, is less than,...) and the third holds the pattern to match +against. + Currently, there are two types of search rules, which are mixed +together into a single class: String-valued and int-valued. The latter +is a hack to enable \verbatim<size>\endverbatim and +\verbatim<age in days>\endverbatim pseudo-header matching. + KMSearchRules should better be organized like KMFilterActions are. + +A filter action (kmfilteraction.h) inherits from KMFilterAction or one +of it's convenience sub-classes. They have three sub-interfaces: (1) +argument handling, (2) processing and (3) parameter widget handling. + Interface (1) consists of args{From,As}String(), name() and +isEmpty() and is used to read and write the arguments (if any) from/to +the config. + Interface (2) is used by the filter manager to execute the action +(process() / ReturnCode). + Interface (3) is used by the filter dialog to allow editing of +actions and consists of name(), label() and the +*ParamWidget*(). Complex parameter widgets are collected in +kmfawidget.{h,cpp}. + +A typical call for applying filters is + +KMKernel::filterMgr() +foreach message { + KMFilterMgr::process(): +} + + +\section configuration CONFIGURE DIALOG + +Files: configuredialog*.{h,cpp} ( identitylistview.{h,cpp} ) + +Contact Marc Mutz <mutz@kde.org> on questions... + +The configuredialog is made up of pages that in turn may consist of a +number of tabs. The genral rule of thumb is that each page and tab is +responsible for reading and writing the config options presented on +it, although in the future, that may be reduced to interacting with +the corresponding config manager instead. But that won't change the +basic principle. + +Thus, there is an abstract base class ConfigurePage (defined in +configuredialog_p.h), which derives from QWidget. It has four methods +of which you have to reimplement at least the first two: + +- void setup() + Re-read the config (from the config file or the manager) and update + the widgets correspondingly. That is, you should only create the + widgets in the ctor, not set the options yet. The reason for that is + that the config dialog, once created, is simply hidden and shown + subsequently, so we need a reset-like method anyway. + +- void apply() + Read the config from the widgets and write it into the config file + or the corresponding config manager. + +- void installProfile() + This is called when the user selected a profile and hit apply. A + profile is just another KConfig object. Therefore, this method + should be the same as setup(), except that you should only alter + widgets for configs that really exist in the profile. + +For tabbed config pages, there exists a convenience class called +TabbedConfigurationPage, which (as of this writing only offers the +addTab() convenience method. It is planned to also provide +reimplementations of setup, dismiss, apply and installProfile that just +call the same functions for each tab. + +\section mdn MDNs + +Files: libkdenetwork/kmime_mdn.{h,cpp} and kmmessage.{h,cpp}, mostly + +Contact Marc Mutz <mutz@kde.org> on questions... + +MDNs (Message Disposition Notifications; RFC 2298) are a way to send +back information regarding received messages back to their +sender. Examples include "message read/deleted/forwarded/processed". + +The code in kmime_mdn.{h,cpp} is responsible for creating the +message/disposition-notification body part (2nd child of +multipart/report that makes the MDN) and for providing the template +for human-readable text that goes into the text/plain part (1st child +of the multipart/report). + +The code in KMMessage::createMDN() actually constructs a message +containing a MDN for this message, using the kmime_mdn helper +functions. It starts by checking the index for an already sent MDN, +since the RFC demands that MDNs be sent only once for every +message. If that test succeeds, it goes on to check various other +constraints as per RFC and if all goes well the message containing the +multipart/report is created. + +If you need to use this functionality, see KMReaderWin::touchMsg() and +KMFilterAction::sendMDN() for examples. The touchMsg() code is invoked +on display of a message and sends a "displayed" MDN back (if so +configured), whereas the KMFilterAction method is a convenience helper +for the various filter actions that can provoke a MDN (move to trash, +redirect, forward, ...). + + +\section folders Folders + +Files: kmfolder*.{h,cpp}, folderstorage.{h,cpp} and *job.{h,cpp} + +Contact Zack Rusin <zack@kde.org> with questions... + +The collaboration among KMail folder classes looks +as follows : + + KMFolderNode + / \ + / \ + KMFolderDir \ + KMFolder + . + . + v + FolderStorage + | + | + KMFolderIndex + | + | + ---< actual folder types: KMFolderImap, KMFolderMbox... >-- + +At the base KMail's folder design starts with KMFolderNode which +inherits QObject. KMFolderNode is the base class encapsulating +common folder properties such as the name and a boolean signifying whether +the folder is a folder holding mail directly or a KMFolderDir. +KMFolderNode's often do not have an on-disk representation, they are +entities existing only within KMail's design. + +KMFolder acts as the runtime representation of a folder with the physical +storage part being represented by a member of type FolderStorage. +KMFolder and FolderStorage have many functions with the same names and +signatures, but there is no inheritance. +KMFolderIndex contains some common indexing functionality for physical folders. +Subclasses of KMFolderIndex finally interact directly with physical storage +or with storage providers over the network. + +KMFolderDir is a directory abstraction which holds KMFolderNode's. +It inherits KMFolderNode and KMFolderNodeList which is a QPtrList<KMFolderNode>. +A special case of a KMFolderDir is KMFolderRootDir; it represents +the toplevel KMFolderDir in KMail's folder hierarchy. + +KMFolderDir's contents are managed by KMFolderMgr's. +KMail contains three main KMFolderMgr's. They can be +accessed via KMKernel ( the "kmkernel" construct ). Those methods are : +1) KMFolderMgr *folderMgr() - which returns the folder manager for + the folders stored locally. +2) KMFolderMgr *imapFolderMgr() - which returns the folder manager + for all imap folders. They're handled a little differently because + for all imap messages only headers are cached locally while the + main contents of all messages is kept on the server. +3) KMFolderMgr *dimapFolderMgr() - which returns disconnected IMAP (dimap) + folder manager. In dimap, both the headers and a copy of the full message + are cached locally. +4) KMFolderMgr *searchFolderMgr() - which returns the folder manager + for search folders (folders created by using the "find + messages" tool). Other email clients call this type of folder + "virtual folders". + +FolderJob classes - These classes allow asynchronous operations on +KMFolder's. You create a Job on the heap, connect to one of its +signals and wait for the job to finish. Folders serve as FolderJob +factories. For example, to retrieve the full message from a folder +you do : + +FolderJob *job = folderParent->createJob( aMsg, tGetMessage ); +connect( job, SIGNAL(messageRetrieved(KMMessage*)), + SLOT(msgWasRetrieved(KMMessage*)) ); +job->start(); + + +\section index Index (old) + +Files: kmfolderindex.{h,cpp} and kmmsg{base,info}.{h,cpp} + +Contact Marc Mutz <mutz@kde.org> or + Till Adam <adam@kde.org> or + Don Sanders <sanders@kde.org> +with questions... + + index := header *entry + + + header := magic LF NUL header-length byte-order-marker sizeof-long + + magic := "# KMail-Index V" 1*DIGITS + + header-length := Q_UINT32 + + byte-order-marker := Q_UINT32( 0x12345678 ) + + sizeof-long := Q_UINT32( 4 / 8 ) + + + entry := tag length value + + tag := Q_UINT32 ; little endian (native?) + + length := Q_UINT16 ; little endian (native?) + + value := unicode-string-value / ulong-value + + unicode-string-value := 0*256QChar ; network-byte-order + + ulong-value := unsigned_long ; little endian + +Currently defined tag values are: + + Msg*Part num. val type obtained by: + + No 0 u - + From 1 u fromStrip().stripWhitespace() + Subject 2 u subject().stripWhitespace() + To 3 u toStrip().stripWhiteSpace() + ReplyToIdMD5 4 u replyToIdMD5().stripWhiteSpace() + IdMD5 5 u msgIdMD5().stripWhiteSpace() + XMark 6 u xmark().stripWhiteSpace() + Offset 7 l folderOffset() (not only mbox!) + LegacyStatus 8 l mLegacyStatus + Size 9 l msgSize() + Date 10 l date() + File 11 u fileName() (not only maildir!) + CryptoState 12 l (signatureState() << 16) | encryptionState()) + MDNSent 13 l mdnSentState() + ReplyToAuxIdMD5 14 u replyToAuxIdMD5() + StrippedSubject 15 u strippedSubjectMD5().stripWhiteSpace() + Status 16 l status() + + u: unicode-string-value; l: ulong-value + +Proposed new (KDE 3.2 / KMail 1.6) entry format: + + index := header *entry + + entry := sync 1*( tag type content ) crc-tag crc-value + + sync := Q_UINT16 (32?) ; resync mark, some magic bit pattern + ; (together with preceding crc-tag provides + ; 24(40)bits to resync on in case of index + ; corruption) + + tag := Q_UINT8 + + type := Q_UINT8 + + content := variable-length-content / fixed-length-content + + crc-tag := tag type ; tag=CRC16, type=CRC16 + + crc-value := Q_UINT16 ; the CRC16 sum is calculated over all of + ; 1*( tag type content ) + + variable-length-content := length *512byte padding + + padding := *3NUL ; make the string a multiple of 4 octets in length + + fixed-length-content := 1*byte + + length := Q_UINT16 (Q_UINT8?) + + byte := Q_UINT8 + +The type field is pseudo-structured: + +bit: 7 6 5 4 3 2 1 0 + +-----+-----+-----+-----+-----+-----+-----+-----+ + | uniq | chunk | len | + +-----+-----+-----+-----+-----+-----+-----+-----+ + +uniq: 3 bits = max. 8 different types with same chunk size: + + for chunk = (0)00 (LSB(base)=0: octets): + 00(0) Utf8String + 01(0) BitField + 10(0) reserved + 11(0) Extend + + for chunk = (1)00 (LSB(base)=1: 16-octet blocks): + 00(1) MD5(Hash/List) + 01(1) reserved + 10(1) reserved + 11(1) Extend + + for chunk = 01 (shorts): + 000 Utf16String + 001-110 reserved + 111 Extend + + for chunk = 10 (int32): + 000 Utf32String (4; not to be used yet) + 001 Size32 + 010 Offset32 + 011 SerNum/UOID + 100 DateTime + 101 Color (QRgb: (a,r,g,b)) + 110 reserved + 111 Extend + + for chunk = 11 (int64): + 000 reserved + 001 Size64 + 010 Offset64 + 011-110 reserved + 111 Extend + +len: length in chunks + 000 (variable width -> Q_UINT16 with the real width follows) + 001..111: fixed-width data of length 2^len (2^1=2..2^6=128) + +You find all defined values for the type field in indexentrybase.cpp + +Currently defined tags are: + + tag type content + + DateSent DateTime Date: + DateReceived DateTime last Received:'s date-time + FromDisplayName String decoded display-name of first From: addr + ToDisplayName String dto. for To: + CcDisplayName String dto. for Cc: + FromAddrSpecs String possibly IMAA-encoded, comma-separated addr-spec's of From: + ToAddrSpecs String dto. for To: + CcAddrSpecs String dto. for Cc: + Subject String decoded Subject: + BaseSubjectMD5 String md5(utf8(stripOffPrefixes(subject()))) + BodyPeek String body preview + MaildirFile String Filename of maildir file for this messagea + MBoxOffset Offset(64) Offset in mbox file (pointing to From_) + MBoxLength Size(64) Length of message in mbox file (incl. From_) + Size Size(64) rfc2822-size of message (in mbox: excl. From_) + Status BitField (see below) + MessageIdMD5 MD5Hash MD5Hash of _normalized_ Message-Id: + MDNLink SerialNumber SerNum of MDN received for this message + DNSLink SerialNumber SerNUm of DSN received for this message + ThreadHeads SerialNumberList MD5Hash's of all (so far discovered) + _top-level thread parents_ + ThreadParents SerialNumberList MD5Hash's of all (so far discovered) + thread parents + + + "String" is either Utf8String or (Utf16String or Latin1String), + depending on content + +Currently allocated bits for the Status BitField are: + + Bit Value: on(/off) (\\imapflag) + + # "std stati": + 0 New (\\Recent) + 1 Read (\\Seen) + 2 Answered (\\Answered) + 3 Deleted (\\Deleted) + 4 Flagged (\\Flagged) + 5 Draft (\\Draft) + 6 Forwarded + 7 reserved + + # message properties: + 8 HasAttachments + 9 MDNSent ($MDNSent) + 10..11 00: unspecified, 01: Low, 10: Normal, 11: High Priority + 12..15 0001: Queued, 0010: Sent, 0011: DeliveryDelayed, + 0100: Delivered, 0101: DeliveryFailed, + 0110: DisplayedToReceiver, 0111: DeletedByReceiver, + 1001: ProcessedByReceiver, 1010: ForwardedByReceiver, + 1011-1110: reserved + 1111: AnswerReceived + + # signature / encryption state: + 16..19 0001: FullyEncrypted, 0010: PartiallyEncrypted, 1xxx: Problem + 20..23 0001: FullySigned, 0010: PartiallySigned, 1xxx: Problem + + # "workflow stati": + 24..25 01: Important, 10: ToDo, 11: Later (from Moz/Evo) + 26..27 01: Work, 10: Personal, 11: reserved (dto.) + 28..29 01: ThreadWatched, 10: ThreadIgnored, 11: reserved + 30..31 reserved + +All bits and combinations marked as reserved MUST NOT be altered if +set and MUST be set to zero (0) when creating the bitfield. + + +\section headers Headers (Threading and Sorting) + +Contact Till Adam <adam@kde.org> or + Don Sanders <sanders@kde.org> +with questions... + +Threading and sorting is implemented in kmheaders.[cpp|h] and headeritem.[cpp|h] +and involves a handfull of players, namely: + +class KMHeaders: + this is the listview that contains the subject, date etc of each mail. + It's a singleton, which means there is only one, per mainwidget headers + list. It is actually a member of KMMainwidget and accessible there. + +class HeaderItem: + these are the [Q|K]ListViewItem descendend items the KMHeaders listview + consists of. There's one for each message. + +class SortCacheItem: + these are what the threading and sorting as well as the caching of those + operate on. Each is paired with a HeaderItem, such that each holds a + pointer to one, and vice versa, each header item holds a pointer to it's + associated sort cache item. + +.sorted file: + The order of the sorted and threaded (if threading is turned on for this + folder) messages is cached on disk in a file named .$FOLDER.index.sorted + so if, for example the name of a folder is foo, the associated sorting + cache file would be called ".foo.index.sorted". + For each message, its serial number, that of its parent, the length of its + sorting key, and the key itself are written to this file. At the start of + the file several per folder counts and flags are cached additionally, + immediately after a short file headers. The entries from the start of the + file are: + - "## KMail Sort V%04d\n\t" (magic header and version string) + - byteOrder flag containing 0x12345678 for byte order testing + - column, the sort column used for the folder + - ascending, the sort direction + - threaded, is the view threaded or is it not? + - appended, have there been items appended to the file (obsolete?) + - discovered_count, number of new mail discovered since the last sort file + generation + - sorted_count, number of sorted messages in the header list + +What is used for figuring out threading? + - messages can have an In-Reply-To header that contains the message id of + another message. This is called a perfect parent. + - additionally there is the References header which, if present, holds a + list of message ids that the current message is a follow up to. We + currently use the second to last entry in that list only. See further + down for the reasoning behind that. + - If the above two fail and the message is prefixed (Re: foo, Fwd: foo etc.) + an attempt is made to find a parent by looking for messages with the same + subject. How that is done is explained below as well. + + For all these comparisons of header contents, the md5 hashes of the headers + are used for speed reasons. They are stored in the index entry for each + message. All data structures described below use md5 hash strings unless + stated otherwise. + +Strategy: + When a folder is opened, updateMessageList is called, which in turn calls + readSortOrder where all the fun happens. If there is a .sorted file at the + expected location, it is openend and parsed. The header flags are read in + order to figure out the state in which this .sorted file was written. This + means the sort direction, whether the folder is threaded or not etc. + FIXME: is there currently sanity checking going on? + Now the file is parsed and for each message in the file a SortCacheItem is + created, which holds the offset in the .sorted file for this message as well + as it's sort key as read from the file. That sort cache item is entered into + the global mSortCache structure (member of the KMHeaders instance), which is + a QMemArray<SortCacheItem *> of the size mFolder->count(). Note that this + is the size reported by the folder, not as read from the .sorted file. The + mSortCache (along with some other structures) is updated when messages are + added or removed. More on that further down. + As soon as the serial number is read from the file, that number is looked up + in the message dict, to ensure it is still in the current folder. If not, it + has been moved away in the meantime (possibly by outside forces such as + other clients) and a deleted counter is incremented and all further + processing stopped for this message. + The messages parent serial number, as read from the sorted file is then + used to look up the parent and reset it to -1 should it not be in the + current folder anymore. -1 and -2 are special values that can show up + as parent serial numbers and are used to encode the following: + -1 means this message has no perfect parent, a parent for it needs to + be found from among the other messages, if there is a suitable one + -2 means this message is top level and will forever stay so, no need + to even try to find a parent. This is also used for the non-threaded + case. These are messages that have neither an In-Reply-To header nor + a References header and have a subject that is not prefixed. + In case there is a perfect parent, the current sort cache item is + appended to the parents list of unsorted children, or to that of + root, if there is not. A sort cache item is created in the mSortCache + for the parent, if it is not already there. Messages with a parent of + -1 are appended to the "unparented" list, which is later traversed and + its elements threaded. Messages with -2 as the parent are children of + root as well, as noted above, and will remain so. + + Once the end of the file is reached, we should have a nicely filled + mSortCache, containing a sort cache item for each message that was in the + sorted file. Messages with perfect parents know about them, top level + messages know about that as well, all others are on a list and will be + threaded later. + + Now, what happens when messages have been added to the store since the last + update of the .sorted file? Right, they don't have a sort cache item yet, + and would be overlooked. Consequently all message ids in the folder from 0 + to mFolder->count() are looked at and a SortCacheItem is created for the + ones that do not have one yet. This is where all sort cache items are created + if there was no sorted file. The items created here are by definition un- + sorted as well as unparented. On creation their sort key is figured out as + well. + + The next step is finding parents for those messages that are either new, or + had a parent of -1 in the .sorted file. To that end, a dict of all sort + cache items indexed by the md5 hash of their messsage id headers is created, + that will be used for looking up sort cache items by message id. The list of + yet unparented messages is then traversed and findParent() called for each + element wihch checks In-Reply-To and References headers and looks up the + sort cache item of those parents in the above mentioned dict. Should none be + found, the item is added to a second list the items of which will be subject + threaded. + + How does findParent() work, well, it tries to find the message pointed to by + the In-Reply-To header and if that fails looks for the one pointed to by the + second to last entry of the References header list. Why the second to last? + Imagine the following situation in Bob's kmail: + - Bob get's mail from Alice + - Bob replies to Alice's mail, but his mail is stored in the Outbox, not the + Inbox. + - Alice replies again, so Bob now has two mails from Alice which are part of + the same thread. The mail in the middle is somewhere else. We want that to + thread as follows: + Bob <- In-Reply-To: Alice1 + ============ + Alice1 + |_Alice <- In-Reply-To: Bob (not here), References: Alice, Bob + + - since the above is a common special case, it is worth trying. I think. ;) + + If the parent found is perfect (In-Reply-To), the sort cache items is marked + as such. mIsImperfectlyThreaded is used for that purposer, we will soon see + why that flag is needed. + + On this first pass, no subject threading is attempted yet. Once it is done, + the messages that are now top-level, the current thread heads, so to speak, + are collected into a second dict ( QDict< QPtrList< SortCacheItem > > ) + that contains for each different subject an entry holding a list of (so far + top level) messages with that subject, that are potential parents for + threading by subjects. These lists are sorted by date, so the parent closest + by date can be chosen. Sorting of these lists happens via insertion sort + while they are built because not only are they expected to be short (apart + from hard corner cases such as cvs commit lists, for which subject threading + makes little sense anyhow and where it should be turned off), but also since + the messages should be roughly pre sorted by date in the store already. + Some cursory benchmarking supports that assumption. + If now a parent is needed for a message with a certain subject, the list of + top level messages with that subject is traversed and the first one that is + older than our message is chosen as it's parent. Parents more than six weeks + older than the message are not accepted. The reasoning being that if a new + message with the same subject turns up after such a long time, the chances + that it is still part of the same thread are slim. The value of six weeks + is chosen as a result of a poll conducted on #kde-devel, so it's probably + bogus. :) All of this happens in the aptly named: findParentBySubject(). + + Everthing that still has no parent after this ends up at toplevel, no further + attemp is made at finding one. If you are reading this because you want to + implement threading by body search or somesuch, please go away, I don't like + you. :) + + Ok, so far we have only operated on sort cache items, nothing ui wise has + happened yet. Now that we have established the parent/child relations of all + messages, it's time to create HeaderItems for them for use in the header + list. But wait, you say, what about sorting? Wouldn't it make sense to do + that first? Exactly, you're a clever bugger, ey? Here, have a cookie. ;) + Both creation of header items and sorting of the as of yet unsorted sort + cache items happen at the same time. + + As previously mentioned (or not) each sort cache item holds a list of its + sorted and one of its unsorted children. Starting with the root node the + unsorted list is first qsorted, and then merged with the list of already + sorted children. To achieve that, the heads of both lists are compared and + the one with the "better" key is added to the list view next by creating a + KMHeaderListItem for it. That header item receives both its sort key as well + as its id from the sort cache item. Should the current sort cache item have + children, it is added to the end of a queue of nodes to repeat these steps + on after the current level is sorted. This way, a breadth first merge sort + is performed on the sort cache items and header items are created at each + node. + + What follows is another iteration over all message ids in the folder, to + make sure that there are HeaderItems for each now. Should that not be the + case, top level emergency items are created. This loop is also used to add + all header items that are imperfectly threaded to a list so they can be + reevalutated when a new message arrives. Here the reverse mapping from + header items to sort cache items are made as well. Those are also necessary + so the sort cache item based data structures can be updated when a message + is removed. + + The rest of readSortOrder should make sense on itself, I guess, if not, drop + me an email. + +What happens when a message arrives in the folder? + Among other things, the msgAdded slot is called, which creates the necessary + sort cache item and header item for the new message and makes sure the data + structures described above are updated accordingly. If threading is enabled, + the new message is threaded using the same findParent and findParentBySubject + methods used on folder open. If the message ends up in a watched or ignored + thread, those status bits are inherited from the parent. The message is also + added to the dict of header items, the index of messages by message id and, + if applicable and if the message is threaded at top level, to the list of + potential parents for subject threading. + + After those house keeping tasks are performed, the list of as of yet imper- + fectly threaded messages is traversed and our newly arrived message is + considered as a new parent for each item on it. This is especially important + to ensure that parents arriving out of order after their children still end + up as parents. If necessary, the entries in the .sorted file of rethreaded + messages are updated. An entry for the new message itself is appended to the + .sorted file as well. + + Note that as an optimization newly arriving mail as part of a mailcheck in + an imap folder is not added via msgAdded, but rather via complete reload of + the folder via readSortOrder(). That's because only the headers are gotten + for each mail on imap and that is so fast, that adding them individually to + the list view is very slow by comparison. Thus we need to make sure that + writeSortOrder() is called whenever something related to sorting changes, + otherwise we read stale info from the .sorted file. The reload is triggered + by the folderComplete() signal of imap folders. + +What happens when a message is removed from the folder? + In this case the msgRemoved slot kicks in and updates the headers list. First + the sort cache item and header item representing our message are removed from + the data structures and the ids of all items after it in the store decre- + mented. Then a list of children of the message is assembled containing those + children that have to be reparented now that our message has gone away. If + one of those children has been marked as toBeDeleted, it is simply added to + root at top level, because there is no need to find a parent for it if it is + to go away as well. This is an optimization to avoid rethreading all + messages in a thread when deleting several messages in a thread, or even the + whole thread. The KMMoveCommand marks all messages that will be moved out of + the folder as such so that can be detected here and the useless reparenting + can be avoided. Note that that does not work when moving messages via filter + action. + + That list of children is then traversed and a new parent found for each one + using, again, findParent and findParentBySubject. When a message becomes + imperfectly threaded in the process, it is added to the corresponding list. + + The message itself is removed from the list of imperfectly threaded messages + and its header item is finally deleted. The HeaderItem destructor destroys + the SortCacheItem as well, which is hopefully no longer referenced anywhere + at this point. + + + +\section display DISPLAY (reader window - new) + + Contact Marc Mutz <mutz@kde.org> with questions... + +What happens when a message is to displayed in the reader window? + First, since KMMessagePart and KMMessage don't work with nested body + parts, a hierarchical tree of MIME body parts is constructed with + partNode's (being wrappers around KMMessagePart and + DwBodyPart). This is done in KMReaderWin::parseMsg(KMMessage*). + After some legacy handling, an ObjectTreeParser is instantiated and + it's parseObjectTree() method called on the root + partNode. ObjectTreeParser is the result of an ongoing refactoring + to enhance the design of the message display code. It's an + implementation of the Method Object pattern, used to break down a + huuuge method into many smaller ones without the need to pass around + a whole lot of paramters (those are made members + instead). parseObjectTree() recurses into the partNode tree and + generates an HTML representation of each node in the tree (although + some can be hidden by setting them to processed beforehand or - the + new way - by using AttachmentStrategy::Hidden). The HTML generation + is delegated to BodyPartFormatters, while the HTML is written to + HTMLWriters, which abstract away HTML sinks. One of those is + KHTMLPartHTMLWriter, which writes to the KHTMLPart in the + readerwindow. This is the current state of the code. The goal of the + ongoing refactoring is to make the HTML generation blissfully + ignorant of the readerwindow and to allow display plugins that can + operate against stable interfaces even though the internal KMail + classes change dramatically. + + To this end, we designed a new set of interfaces that allows plugins + to be written that need or want to asynchronously update their HTML + representation. This set of interfaces consists of the following: + + - @em BodyPartFormatterPlugin + the plugin base class. Allows the BodyPartFormatterFactory to + query it for an arbitray number of BodyPartFormatters and + their associated metadata and url handlers. + + - @em BodyPartFormatter + the formatter interface. Contains a single method format() + that takes a BodyPart to process and an HTMLWriter to write the + generated HTML to and returns a result code with which it can + request more information or delegate the formatting back to + some default processor. BodyPartFormatters are meant to be + Flyweights, implying that the format() method is not allowed + to maintain state between calls, but see Mememto below. + + - @em BodyPart + body part interface. Contains methods to retrieve the content + of a message part and some of the more interesting header + information. This interface decouples the bodypart formatters + from the implementation used in KMail (or KNode, for that + matter). Also contains methods to set and retrieve a Memento, + which is used by BodyPartFormatters to maintain state between + calls of format(). + + - @em BodyPartMemento + interface for opaque state storage and event handling. + Contains only a virtual destructor to enable the backend + processing code to destroy the object without the help of the + bodypart formatter. + + +During the design phase we identified a need for BodyPartFormatters to +request their being called on some form of events, e.g. a dcop +signal. Thus, the Memento interface also includes the IObserver and +ISubject interfaces. If a BodyPartFormatter needs to react to a signal +(Qt or DCOP), it implements the Memento interface using a QObject, +connects the signal to a slot on the Memento and (as an ISubject) +notifies it's IObservers when the slot is called. If a Memento is +created, the reader window registers itself as an observer of the +Memento and will magically invoke the corresponding BodyPartFormatter, +passing along the Memento to be retrieved from the BodyPart interface. + +An example should make this clearer. Suppose, we want to update our +display after 10 seconds. Initially, we just write out an icon, and +after 10 seconds, we want to replace the icon by a "Hello world!" +line. The following code does exactly this: + +@code +class DelayedHelloWorldBodyPartFormatter + : public KMail::BodyPartFormatter { +public: + Result format( KMail::BodyPart * bodyPart, + KMail::HTMLWriter * htmlWriter ) { + if ( !bodyPart->memento() ) { + bodyPart->registerMemento( new DelayedHelloWorldBodyPartMemento() ); + return AsIcon; + } else { + htmlWriter->write( "Hello, world!" ); + return Ok; + } + } +}; + +class DelayedHelloWorldBodyPartMemento + : public QObject, public KMail::BodyPartMemento { +public: + DelayedHelloWorldBodyPartMemento() + : QObject( 0, "DelayedHelloWorldBodyPartMemento" ), + KMail::BodyPartMemento() + { + QTimer::singleShot( 10*1000, this, SLOT(slotTimeout()) ); + } + +private slots: + void slotTimeout() { notify(): } + +private: + // need to reimplement this abstract method... + bool update() { return true; } +}; +@endcode + +*/ |