Coverage for sources/mimeogram/formatters.py: 100%

31 statements  

« prev     ^ index     » next       coverage.py v7.9.2, created at 2025-07-05 19:46 +0000

1# vim: set filetype=python fileencoding=utf-8: 

2# -*- coding: utf-8 -*- 

3 

4#============================================================================# 

5# # 

6# Licensed under the Apache License, Version 2.0 (the "License"); # 

7# you may not use this file except in compliance with the License. # 

8# You may obtain a copy of the License at # 

9# # 

10# http://www.apache.org/licenses/LICENSE-2.0 # 

11# # 

12# Unless required by applicable law or agreed to in writing, software # 

13# distributed under the License is distributed on an "AS IS" BASIS, # 

14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 

15# See the License for the specific language governing permissions and # 

16# limitations under the License. # 

17# # 

18#============================================================================# 

19 

20 

21''' Formatting of mimeogram bundles. ''' 

22 

23 

24from . import __ 

25from . import parts as _parts 

26 

27 

28def format_mimeogram( 

29 parts: __.cabc.Sequence[ _parts.Part ], 

30 message: __.typx.Optional[ str ] = None, 

31 deterministic_boundary: bool = False, 

32) -> str: 

33 ''' Formats parts into mimeogram. ''' 

34 if not parts and message is None: 

35 from .exceptions import MimeogramFormatEmpty 

36 raise MimeogramFormatEmpty( ) 

37 if deterministic_boundary: 

38 content_hash = _compute_content_hash( parts, message ) 

39 boundary = f"====MIMEOGRAM_{content_hash}====" 

40 else: 

41 boundary = "====MIMEOGRAM_{uuid}====".format( uuid = __.uuid4( ).hex ) 

42 lines: list[ str ] = [ ] 

43 if message: 

44 message_part = _parts.Part( 

45 location = 'mimeogram://message', 

46 mimetype = 'text/plain', # TODO? Markdown 

47 charset = 'utf-8', 

48 linesep = _parts.LineSeparators.LF, 

49 content = message ) 

50 lines.append( format_part( message_part, boundary ) ) 

51 for part in parts: 

52 lines.append( format_part( part, boundary ) ) # noqa: PERF401 

53 lines.append( f"--{boundary}--" ) 

54 return '\n'.join( lines ) 

55 

56 

57def format_part( part: _parts.Part, boundary: str ) -> str: 

58 ''' Formats part with boundary marker and headers. ''' 

59 return '\n'.join( ( 

60 f"--{boundary}", 

61 f"Content-Location: {part.location}", 

62 f"Content-Type: {part.mimetype}; " 

63 f"charset={part.charset}; " 

64 f"linesep={part.linesep.name}", 

65 '', 

66 part.content ) ) 

67 

68 

69def _compute_content_hash( 

70 parts: __.cabc.Sequence[ _parts.Part ], 

71 message: __.typx.Optional[ str ] = None, 

72) -> str: 

73 ''' Computes deterministic hash for mimeogram content. ''' 

74 hasher = __.hashlib.sha256( ) 

75 if message is not None: 

76 hasher.update( message.encode( 'utf-8' ) ) 

77 for part in parts: 

78 hasher.update( str( part.location ).encode( 'utf-8' ) ) 

79 hasher.update( str( part.mimetype ).encode( 'utf-8' ) ) 

80 hasher.update( str( part.charset ).encode( 'utf-8' ) ) 

81 hasher.update( str( part.linesep.name ).encode( 'utf-8' ) ) 

82 hasher.update( str( part.content ).encode( 'utf-8' ) ) 

83 return hasher.hexdigest( )