/// /// Copyright © 2003-2008 JetBrains s.r.o. /// You may distribute under the terms of the GNU General Public License, as published by the Free Software Foundation, version 2 (see License.txt in the repository root folder). /// using System; using System.Diagnostics; using System.Text; using JetBrains.DataStructures; using JetBrains.Omea.Conversations; using JetBrains.Omea.OpenAPI; using JetBrains.Omea.Base; using JetBrains.Omea.MIME; using JetBrains.Omea.ResourceTools; namespace JetBrains.Omea.Nntp { /** * processing article: parsing headers, parsing multi-part bodies, * creating attachments, etc */ internal delegate void CreateArticleDelegate( string[] lines, IResource groupRes, string articleId ); internal delegate void CreateArticleByProtocolHandlerDelegate( string[] lines, IResource article ); internal delegate void CreateArticleFromHeadersDelegate( string line, IResource groupRes ); internal class NewsArticleParser { /** * all article's processing should be performed in the resource thread * this function is called if article is downloaded from a group */ public static void CreateArticle( string[] lines, IResource groupRes, string articleId ) { // if user has already unsubscribed from the group or the // groups is already deleted then skip the article IResource server = new NewsgroupResource( groupRes ).Server; if( server == null ) { return; } articleId = ParseTools.EscapeCaseSensitiveString( articleId ); // is article deleted? if( NewsArticleHelper.IsArticleDeleted( articleId ) ) { return; } PrepareLines( lines ); try { ServerResource serverRes = new ServerResource( server ); string charset = serverRes.Charset; bool bodyStarted = false; string line; string content_type = string.Empty; string content_transfer_encoding = string.Empty; IContact sender = null; DateTime date = DateTime.MinValue; bool mySelf = false; bool newArticle; IResource article = Core.ResourceStore.FindUniqueResource( NntpPlugin._newsArticle, NntpPlugin._propArticleId, articleId ); if( article != null ) { if( !article.HasProp( NntpPlugin._propHasNoBody ) ) { return; } article.BeginUpdate(); newArticle = false; } else { article = Core.ResourceStore.BeginNewResource( NntpPlugin._newsArticle ); newArticle = true; } for( int i = 0; i < lines.Length; ++i ) { line = lines[ i ]; if( line == null ) { continue; } if( bodyStarted ) { _bodyBuilder.Append( line ); } else { _headersBuilder.Append( line ); _headersBuilder.Append( "\r\n" ); if( Utils.StartsWith( line, "from: ", true ) ) { string from = line.Substring( 6 ); article.SetProp( NntpPlugin._propRawFrom, from ); mySelf = ParseFrom( article, TranslateHeader( charset, from ), out sender ); UpdateLastCorrespondDate( sender, date ); } else if( Utils.StartsWith( line, "subject: ", true ) ) { string subject = line.Substring( 9 ); article.SetProp( NntpPlugin._propRawSubject, subject ); article.SetProp( Core.Props.Subject, TranslateHeader( charset, subject ) ); } else if( Utils.StartsWith( line, "message-id: ", true ) ) { ParseMessageId( article, ParseTools.EscapeCaseSensitiveString( line.Substring( 12 ) ), articleId ); } else if( Utils.StartsWith( line, "newsgroups: ", true ) ) { if( line.IndexOf( ',' ) > 12 ) { ParseNewsgroups( article, groupRes, serverRes, line.Substring( 12 ) ); } } else if( Utils.StartsWith( line, "date: ", true ) ) { date = ParseDate( article, line.Substring( 6 ) ); UpdateLastCorrespondDate( sender, date ); } else if( Utils.StartsWith( line, "references: ", true ) ) { ParseReferences( article, line.Substring( 12 ) ); } else if( Utils.StartsWith( line, "content-type: ", true ) ) { content_type = line.Substring( 14 ); } else if( Utils.StartsWith( line, "followup-to: ", true ) ) { article.SetProp( NntpPlugin._propFollowupTo, line.Substring( 13 ) ); } else if( Utils.StartsWith( line, "content-transfer-encoding: ", true ) ) { content_transfer_encoding = line.Substring( 27 ); } else if( line == "\r\n" ) { bodyStarted = true; } } } ProcessBody( _bodyBuilder.ToString(), content_type, content_transfer_encoding, article, charset ); article.SetProp( NntpPlugin._propArticleHeaders, _headersBuilder.ToString() ); article.AddLink( NntpPlugin._propTo, groupRes ); if( article.GetPropText( NntpPlugin._propArticleId ).Length == 0 ) { article.SetProp( NntpPlugin._propArticleId, articleId ); } if( newArticle ) { article.SetProp( NntpPlugin._propIsUnread, true ); IResourceList categories = Core.CategoryManager.GetResourceCategories( groupRes ); foreach( IResource category in categories ) { Core.CategoryManager.AddResourceCategory( article, category ); } Core.FilterEngine.ExecRules( StandardEvents.ResourceReceived, article ); CleanLocalArticle( articleId, article ); } article.EndUpdate(); CheckArticleInIgnoredThreads( article ); CheckArticleInSelfThread( article ); if( mySelf && serverRes.MarkFromMeAsRead ) { article.BeginUpdate(); article.DeleteProp( NntpPlugin._propIsUnread ); article.EndUpdate(); } Core.TextIndexManager.QueryIndexing( article.Id ); } finally { DisposeStringBuilders(); } } /** * this function is called if article is downloaded from a group * article should be a newly created transient resource */ public static void CreateArticleByProtocolHandler( string[] lines, IResource article ) { if( !article.IsTransient ) { throw new ArgumentException( "Article should be a newly created transient resource", "article" ); } string articleId = article.GetPropText( NntpPlugin._propArticleId ); if( Core.ResourceStore.FindUniqueResource( NntpPlugin._newsArticle, NntpPlugin._propArticleId, articleId ) != null ) { return; } PrepareLines( lines ); try { IResource server = article.GetLinkProp( NntpPlugin._propTo ); string charset = new ServerResource( server ).Charset; bool bodyStarted = false; string line; string content_type = string.Empty; string content_transfer_encoding = string.Empty; IContact sender = null; DateTime date = DateTime.MinValue; bool mySelf = false; for( int i = 0; i < lines.Length; ++i ) { line = lines[ i ]; if( line == null ) { continue; } if( bodyStarted ) { _bodyBuilder.Append( line ); } else { _headersBuilder.Append( line ); _headersBuilder.Append( "\r\n" ); if( Utils.StartsWith( line, "from: ", true ) ) { string from = line.Substring( 6 ); article.SetProp( NntpPlugin._propRawFrom, from ); mySelf = ParseFrom( article, TranslateHeader( charset, from ), out sender ); UpdateLastCorrespondDate( sender, date ); } else if( Utils.StartsWith( line, "subject: ", true ) ) { string subject = line.Substring( 9 ); article.SetProp( NntpPlugin._propRawSubject, subject ); article.SetProp( Core.Props.Subject, TranslateHeader( charset, subject ) ); } else if( Utils.StartsWith( line, "message-id: ", true ) ) { ParseMessageId( article, ParseTools.EscapeCaseSensitiveString( line.Substring( 12 ) ), articleId ); } else if( Utils.StartsWith( line, "newsgroups: ", true ) ) { Subscribe2Groups( article, line.Substring( 12 ) ); } else if( Utils.StartsWith( line, "date: ", true ) ) { date = ParseDate( article, line.Substring( 6 ) ); UpdateLastCorrespondDate( sender, date ); } else if( Utils.StartsWith( line, "references: ", true ) ) { ParseReferences( article, line.Substring( 12 ) ); } else if( Utils.StartsWith( line, "content-type: ", true ) ) { content_type = line.Substring( 14 ); } else if( Utils.StartsWith( line, "followup-to: ", true ) ) { article.SetProp( NntpPlugin._propFollowupTo, line.Substring( 13 ) ); } else if( Utils.StartsWith( line, "content-transfer-encoding: ", true ) ) { content_transfer_encoding = line.Substring( 27 ); } else if( line == "\r\n" ) { bodyStarted = true; } } } ProcessBody( _bodyBuilder.ToString(), content_type, content_transfer_encoding, article, charset ); article.SetProp( NntpPlugin._propArticleHeaders, _headersBuilder.ToString() ); article.SetProp( NntpPlugin._propIsUnread, true ); Core.FilterEngine.ExecRules( StandardEvents.ResourceReceived, article ); CleanLocalArticle( articleId, article ); article.EndUpdate(); if( mySelf && new ServerResource( server ).MarkFromMeAsRead ) { article.BeginUpdate(); article.DeleteProp( NntpPlugin._propIsUnread ); article.EndUpdate(); } Core.TextIndexManager.QueryIndexing( article.Id ); } finally { DisposeStringBuilders(); } } public static void CreateArticleFromHeaders( string line, IResource groupRes ) { string[] headers = line.Split( '\t' ); if( headers.Length <= 5 ) { return; } string articleId = ParseTools.EscapeCaseSensitiveString( headers[ 4 ] ); if( articleId.Length == 0 || NewsArticleHelper.IsArticleDeleted( articleId ) ) { return; } // if user has already unsubscribed from the group or the // groups is already deleted then skip the article IResource server = new NewsgroupResource( groupRes ).Server; if( server == null ) { return; } // is article deleted? if( NewsArticleHelper.IsArticleDeleted( articleId ) ) { return; } IResource article; IContact sender; bool mySelf; bool newArticle; string charset = new ServerResource( server ).Charset; article = Core.ResourceStore.FindUniqueResource( NntpPlugin._newsArticle, NntpPlugin._propArticleId, articleId ); if( article != null ) { article.BeginUpdate(); newArticle = false; } else { article = Core.ResourceStore.BeginNewResource( NntpPlugin._newsArticle ); newArticle = true; } article.SetProp( NntpPlugin._propHasNoBody, true ); NewsArticleHelper.SetArticleNumber( article, groupRes, headers[ 0 ] ); DateTime date = ParseDate( article, headers[ 3 ] ); string from = headers[ 2 ]; article.SetProp( NntpPlugin._propRawFrom, from ); mySelf = ParseFrom( article, TranslateHeader( charset, from ), out sender ); string subject = headers[ 1 ]; article.SetProp( NntpPlugin._propRawSubject, subject ); article.SetProp( Core.Props.Subject, TranslateHeader( charset, subject ) ); UpdateLastCorrespondDate( sender, date ); ParseMessageId( article, articleId, articleId ); string references = headers[ 5 ]; if( references.Length > 0 ) { ParseReferences( article, references ); } article.AddLink( NntpPlugin._propTo, groupRes ); if( newArticle ) { article.SetProp( NntpPlugin._propIsUnread, true ); IResourceList categories = Core.CategoryManager.GetResourceCategories( groupRes ); foreach( IResource category in categories ) { Core.CategoryManager.AddResourceCategory( article, category ); } Core.FilterEngine.ExecRules( StandardEvents.ResourceReceived, article ); CleanLocalArticle( articleId, article ); } article.EndUpdate(); if( mySelf && new ServerResource( server ).MarkFromMeAsRead ) { article.BeginUpdate(); article.DeleteProp( NntpPlugin._propIsUnread ); article.EndUpdate(); } Core.TextIndexManager.QueryIndexing( article.Id ); } /** * preparing article lines: gathering line-broken headers, * trimimg, replacing tabs in headers with spaces */ private static void PrepareLines( string[] lines ) { string line; int headersLines = 0; // at first, search for body begining for( ; headersLines < lines.Length && lines[ headersLines ] != "\r\n"; ++headersLines ); // remove escaped single periods for( int i = headersLines + 1; i < lines.Length; ++i ) { line = lines[ i ]; if( line.StartsWith( ".." ) ) { lines[ i ] = line.Remove( 0, 1 ); } } // then remove ending CRs for( int i = 0; i < headersLines; ++i ) { lines[ i ] = lines[ i ].TrimEnd( '\r', '\n' ); } // finally, look through headers, combine headers where necessary for( ; headersLines > 1; ) { line = lines[ --headersLines ]; if( line.StartsWith( " " ) || line.StartsWith( "\t" ) ) { string previousLine = lines[ headersLines - 1 ]; lines[ headersLines - 1 ] = previousLine + line.TrimStart( ' ', '\t' ); lines[ headersLines ] = null; } } } /** * returns true if a contact is myself */ internal static bool ParseFrom( IResource article, string fromValue, out IContact contact ) { if( MIMEParser.ContainsMIMEStrings( fromValue ) ) { fromValue = ParseTools.ParseMIMEHeader( fromValue ); } fromValue = fromValue.Replace( "<", null ).Replace( ">", null ).Replace( "\\", null ).Replace( "//", null ); string[] parts = fromValue.Split( ' ' ); string eMail = string.Empty; foreach( string part in parts ) { if( part.IndexOf( '@' ) >= 0 ) { eMail = part; break; } } string displayName = fromValue; if( eMail.Length > 0 ) { displayName = displayName.Replace( eMail, null ).Trim(); } if( eMail.Length > 0 || displayName.Length > 0 ) { IContactManager cm = Core.ContactManager; IResource oldFrom = article.GetLinkProp( cm.Props.LinkFrom ); contact = cm.FindOrCreateContact( eMail, displayName ); cm.LinkContactToResource( cm.Props.LinkFrom, contact.Resource, article, eMail, displayName ); if( oldFrom != null && contact.Resource != oldFrom ) { cm.DeleteUnusedContacts( oldFrom.ToResourceList() ); } return contact.IsMyself; } contact = null; return false; } private static void ParseMessageId( IResource article, string idValue, string idCandidate ) { if( article.HasProp( NntpPlugin._propArticleId ) ) { idValue = article.GetPropText( NntpPlugin._propArticleId ); } else { if( idCandidate != idValue ) { idValue = idCandidate; } article.SetProp( NntpPlugin._propArticleId, idValue ); } IResourceList childs = Core.ResourceStore.FindResources( NntpPlugin._newsArticle, NntpPlugin._propReferenceId, idValue ); foreach( IResource child in childs ) { SetReply( child, article ); UpdateLastThreadArticleDate( article ); } } private static void ParseNewsgroups( IResource article, IResource groupRes, ServerResource serverRes, string newsgroups ) { string[] groups = newsgroups.Split( ',' ); foreach( string group in groups ) { bool groupFound = false; foreach( IResource res in serverRes.Groups ) { if( groupRes != res && String.Compare( group, res.GetPropText( Core.Props.Name ), true ) == 0 && new NewsgroupResource( res ).IsSubscribed ) { article.AddLink( NntpPlugin._propTo, res ); groupFound = true; break; } } if( !groupFound ) { IResource fakeGroup = null; IResourceList fakeGroups = Core.ResourceStore.FindResources( NntpPlugin._newsGroup, Core.Props.Name, group ); if( fakeGroups.Count == 0 ) { fakeGroup = Core.ResourceStore.NewResource( NntpPlugin._newsGroup ); fakeGroup.SetProp( Core.Props.Name, group ); } else { foreach( IResource res in fakeGroups ) { if( fakeGroup == null || res.HasProp( Core.Props.Parent ) ) { fakeGroup = res; if( res.HasProp( Core.Props.Parent ) ) { break; } } } } article.AddLink( NntpPlugin._propTo, fakeGroup ); } } } private static void Subscribe2Groups( IResource article, string newsgroups ) { IResource server = article.GetLinkProp( NntpPlugin._propTo ); if( server != null ) { article.DeleteLink( NntpPlugin._propTo, server ); string[] groups = newsgroups.Split( ',' ); foreach( string group in groups ) { NntpPlugin.Subscribe2Group( group, server ); foreach( IResource groupRes in new ServerResource( server ).Groups ) { if( String.Compare( groupRes.GetPropText( Core.Props.Name ), group, true ) == 0 ) { article.AddLink( NntpPlugin._propTo, groupRes ); break; } } } } } private static DateTime ParseDate( IResource article, string dateValue ) { DateTime date; DateTime threadDate = article.GetDateProp( NntpPlugin._propLastArticleDate ); try { date = RFC822DateParser.ParseDate( dateValue ); article.SetProp( Core.Props.Date, date ); if( threadDate == DateTime.MinValue || threadDate < date ) article.SetProp( NntpPlugin._propLastArticleDate, date ); } catch( Exception e ) { Trace.WriteLine( "Failed to parse RFC-822 date " + dateValue + ": " + e.Message ); date = DateTime.Now; if( !article.HasProp( Core.Props.Date ) ) { article.SetProp( Core.Props.Date, date ); } } return date; } internal static void ParseReferences( IResource article, string refValue ) { string[] refs = refValue.Trim().Split( ' ' ); if( refs.Length > 0 ) { string reference = ParseTools.EscapeCaseSensitiveString( refs[ refs.Length - 1 ] ); article.SetProp( NntpPlugin._propReferenceId, reference ); IResource parentArticle = Core.ResourceStore.FindUniqueResource( NntpPlugin._newsArticle, NntpPlugin._propArticleId, reference ); if( parentArticle != null ) { SetReply( article, parentArticle ); UpdateLastThreadArticleDate( article ); } } } internal static void ProcessBody( string body, string content_type, IResource articleRes, string defaultCharset ) { ProcessBody( body, content_type, string.Empty, articleRes, defaultCharset ); } private static void ProcessBody( string body, string content_type, string content_transfer_encoding, IResource article, string defaultCharset ) { _mimeBodyParser.ProcessBody( body, content_type, content_transfer_encoding, defaultCharset ); // store charset as prop string charset = _mimeBodyParser.Charset; article.SetProp( Core.FileResourceManager.PropCharset, charset ); // correct from for a specific charset string from = article.GetPropText( NntpPlugin._propRawFrom ); IContact sender; ParseFrom( article, TranslateHeader( charset, from ), out sender ); // correct subject for a specific charset string subject = article.GetPropText( NntpPlugin._propRawSubject ); if( subject.Length > 0 ) { article.SetProp( Core.Props.Subject, TranslateHeader( charset, subject ) ); } // walk though article parts -- body & atatchments MessagePart[] parts = _mimeBodyParser.GetParts(); foreach( MessagePart part in parts ) { if( part.PartType == MessagePartTypes.Body ) { article.SetProp( Core.Props.LongBody, part.Body ); article.SetProp( Core.Props.Size, part.Body.Length ); } else if( part.PartType == MessagePartTypes.HtmlBody ) { article.SetProp( NntpPlugin._propHtmlContent, part.Body ); } else { /** * forbid adding attachments for the LocalArticles resources */ if( article.Type != NntpPlugin._newsLocalArticle ) { string extension = IOTools.GetExtension( part.Name ); string resourceType = Core.FileResourceManager.GetResourceTypeByExtension( extension ); if( resourceType == null ) { resourceType = NntpPlugin._unknownFileResourceType; } IResource attachment = Core.ResourceStore.BeginNewResource( resourceType ); try { attachment.SetProp( Core.Props.Name, part.Name ); attachment.SetProp( Core.Props.Size, (int) part.Content.Length ); attachment.SetProp( NntpPlugin._propContent, part.Content ); if( part.PartType == MessagePartTypes.Inline ) { attachment.SetProp( NntpPlugin._propInlineAttachment, true ); } else if( part.PartType == MessagePartTypes.Embedded ) { attachment.SetProp( CommonProps.ContentId, part.ContentId ); attachment.AddLink( NntpPlugin._propEmbeddedContent, article ); continue; } attachment.AddLink( NntpPlugin._propAttachment, article ); } finally { attachment.EndUpdate(); Core.TextIndexManager.QueryIndexing( attachment.Id ); } } } } if( !article.HasProp( Core.Props.LongBody ) && !article.HasProp( NntpPlugin._propHtmlContent ) ) { article.SetProp( Core.Props.LongBody, " " ); article.SetProp( Core.Props.Size, 0 ); } article.DeleteProp( NntpPlugin._propHasNoBody ); NewsArticleHelper.RemoveArticleNumbers( article ); _mimeBodyParser.Clear(); } internal static string TranslateHeader( string charset, string header ) { header = MIMEParser.ContainsMIMEStrings( header ) ? ParseTools.ParseMIMEHeader( header ) : MIMEParser.TranslateRawStringInCharset( charset, header ); return header; } private static void UpdateLastCorrespondDate( IContact sender, DateTime date ) { if( sender != null ) { if( ProductType.Reader == NntpPlugin._productType ) { IResource contactRes = sender.Resource; if( contactRes.GetDateProp( Core.ContactManager.Props.LastCorrespondenceDate ) < date ) { contactRes.SetProp( Core.ContactManager.Props.LastCorrespondenceDate, date ); } } } } /// /// Set the date of the last thread article to all its roots along the /// hierarchy. /// internal static void UpdateLastThreadArticleDate( IResource article ) { DateTime val = article.GetDateProp( NntpPlugin._propLastArticleDate ); IResourceList roots = article.GetLinksFrom( NntpPlugin._newsArticle, NntpPlugin._propReply ); while( roots.Count > 0 ) { DateTime rootVal = roots[ 0 ].GetDateProp( NntpPlugin._propLastArticleDate ); if( rootVal == DateTime.MinValue || rootVal < val ) roots[ 0 ].SetProp( NntpPlugin._propLastArticleDate, val ); roots = roots[ 0 ].GetLinksFrom( NntpPlugin._newsArticle, NntpPlugin._propReply ); } } /// /// When new article comes check whether it is linked to the thread /// which was paused for updates. In such case, simply delete the /// article (non-permanently, through registered IResourceDeleter). /// private static void CheckArticleInIgnoredThreads( IResource article ) { IResource root; bool ignore = ConversationBuilder.CheckPropOnParents( article, NntpPlugin._propIsIgnoredThread, out root ); if( ignore ) { // We have not only to delete this article, but also check // downwards the thread because it is possible for replies // to be downloaded before the source article. IResourceDeleter deleter = Core.PluginLoader.GetResourceDeleter( article.Type ); deleter.DeleteResource( article ); DateTime ignoreStartDate = root.GetDateProp( NntpPlugin._propThreadVisibilityToggleDate ); IResourceList thread = ConversationBuilder.UnrollConversation( article ); foreach( IResource res in thread ) { DateTime dateTime = res.GetDateProp( Core.Props.Date ); if( !res.HasProp( Core.Props.IsDeleted ) && dateTime > ignoreStartDate ) { deleter.UndeleteResource( res ); } } } } private static void CheckArticleInSelfThread( IResource article ) { IResource from = article.GetLinkProp( Core.ContactManager.Props.LinkFrom ); if( from != null && from.HasProp( Core.ContactManager.Props.Myself ) ) { article.SetProp( NntpPlugin._propIsSelfThread, true ); } else { IResource root; bool hasProp = ConversationBuilder.CheckPropOnParents( article, NntpPlugin._propIsSelfThread, out root ); if( hasProp ) { article.SetProp( NntpPlugin._propIsSelfThread, true ); // We have not only to set property for this article, but // also check downwards the thread because it is possible // for replies to be downloaded before the source article. IResourceList thread = ConversationBuilder.UnrollConversation( article ); foreach( IResource res in thread ) { res.SetProp( NntpPlugin._propIsSelfThread, true ); } } } } private static void CleanLocalArticle( string articleId, IResource articleRes ) { IResource localArticle = Core.ResourceStore.FindUniqueResource( NntpPlugin._newsLocalArticle, NntpPlugin._propArticleId, articleId ); if( localArticle != null ) { foreach( IResource wsp in localArticle.GetLinksOfType( "Workspace", "WorkspaceVisible" ) ) { Core.WorkspaceManager.AddResourceToWorkspace( wsp, articleRes ); } Core.ContactManager.UnlinkContactInformation( localArticle ); localArticle.Delete(); NewsFolders.PlaceResourceToFolder( articleRes, NewsFolders.SentItems ); } } private static IntHashSet _convsParents = new IntHashSet( 16 ); private static void SetReply( IResource child, IResource article ) { try { _convsParents.Add( child.Id ); IResource res = article; while( res != null ) { int id = res.Id; if( _convsParents.Contains( id ) ) { return; } _convsParents.Add( id ); res = res.GetLinkProp( NntpPlugin._propReply ); } child.SetProp( NntpPlugin._propReply, article ); } finally { _convsParents.Clear(); } } private static void DisposeStringBuilders() { _bodyBuilder.Length = 0; if( _bodyBuilder.Capacity > 16384 ) { _bodyBuilder.Capacity = 1024; } _headersBuilder.Length = 0; if( _headersBuilder.Capacity > 16384 ) { _headersBuilder.Capacity = 1024; } } private static StringBuilder _bodyBuilder = new StringBuilder(); private static StringBuilder _headersBuilder = new StringBuilder(); private static MultiPartBodyParser _mimeBodyParser = new MultiPartBodyParser(); } }