Просмотр исходного кода

only transcode messages to ICQ recipients

Mike 1 неделя назад
Родитель
Сommit
ee7b495fe8

+ 13 - 25
foodgroup/icbm.go

@@ -11,7 +11,6 @@ import (
 	"strings"
 	"strings"
 	"sync"
 	"sync"
 	"time"
 	"time"
-	"unicode"
 
 
 	"github.com/mk6i/open-oscar-server/state"
 	"github.com/mk6i/open-oscar-server/state"
 	"github.com/mk6i/open-oscar-server/wire"
 	"github.com/mk6i/open-oscar-server/wire"
@@ -201,10 +200,9 @@ func (s *ICBMService) ChannelMsgToHost(ctx context.Context, instance *state.Sess
 			}
 			}
 		}
 		}
 
 
-		if (clientIM.ChannelID == wire.ICBMChannelIM || clientIM.ChannelID == wire.ICBMChannelMIME) &&
-			tlv.Tag == wire.ICBMTLVAOLIMData {
-			// A TLV that fails to transcode is delivered as sent.
-			if transcoded, err := transcodeMessage(s.legacyCharset, instance.Session().HasCap(wire.CapUTF8Messages), recipSess.HasCap(wire.CapUTF8Messages), tlv); err != nil {
+		if tlv.Tag == wire.ICBMTLVAOLIMData && recipSess.UIN() != 0 &&
+			(clientIM.ChannelID == wire.ICBMChannelIM || clientIM.ChannelID == wire.ICBMChannelMIME) {
+			if transcoded, err := transcodeICQMessage(s.legacyCharset, recipSess.HasCap(wire.CapUTF8Messages), tlv); err != nil {
 				s.logger.WarnContext(ctx, "unable to transcode message", "recipient", recipSess.IdentScreenName(), "err", err)
 				s.logger.WarnContext(ctx, "unable to transcode message", "recipient", recipSess.IdentScreenName(), "err", err)
 			} else {
 			} else {
 				tlv = transcoded
 				tlv = transcoded
@@ -261,14 +259,11 @@ func (s *ICBMService) ChannelMsgToHost(ctx context.Context, instance *state.Sess
 	}, nil
 	}, nil
 }
 }
 
 
-// transcodeMessage recodes message text between UCS-2BE and the legacy 8-bit
-// charset enc when exactly one of sender and recip supports Unicode messages.
-func transcodeMessage(enc encoding.Encoding, senderUnicode bool, recipUnicode bool, tlv wire.TLV) (wire.TLV, error) {
-	if senderUnicode == recipUnicode {
-		// both speak unicode or don't speak unicode, no need to transcode
-		return tlv, nil
-	}
-
+// transcodeICQMessage recodes the ICQ message into Unicode or the fallback encoding
+// depending on the recipient's Unicode support. Recode ASCII/Latin-1 containing
+// non ASCII/Latin-1 bytes into Unicode; Recode Unicode into the fallback encoding
+// if the recipient does not support Unicode.
+func transcodeICQMessage(enc encoding.Encoding, recipUnicode bool, tlv wire.TLV) (wire.TLV, error) {
 	var frags []wire.ICBMCh1Fragment
 	var frags []wire.ICBMCh1Fragment
 	if err := wire.UnmarshalBE(&frags, bytes.NewReader(tlv.Value)); err != nil {
 	if err := wire.UnmarshalBE(&frags, bytes.NewReader(tlv.Value)); err != nil {
 		return tlv, fmt.Errorf("unable to unmarshal ICBM message: %w", err)
 		return tlv, fmt.Errorf("unable to unmarshal ICBM message: %w", err)
@@ -298,14 +293,7 @@ func transcodeMessage(enc encoding.Encoding, senderUnicode bool, recipUnicode bo
 		return wire.NewTLVBE(tlv.Tag, frags), nil
 		return wire.NewTLVBE(tlv.Tag, frags), nil
 	}
 	}
 
 
-	allASCII := true
-	for _, c := range msg.Text {
-		if c > unicode.MaxASCII {
-			allASCII = false
-			break
-		}
-	}
-	if allASCII || !recipUnicode {
+	if wire.AllASCII(string(msg.Text)) || !recipUnicode {
 		return tlv, nil
 		return tlv, nil
 	}
 	}
 
 
@@ -327,7 +315,7 @@ func transcodeFromUnicode(enc encoding.Encoding, msg []byte) ([]wire.ICBMCh1Frag
 	// ReplaceUnsupported substitutes the non-printing ASCII SUB character.
 	// ReplaceUnsupported substitutes the non-printing ASCII SUB character.
 	text = bytes.ReplaceAll(text, []byte{encoding.ASCIISub}, []byte{'?'})
 	text = bytes.ReplaceAll(text, []byte{encoding.ASCIISub}, []byte{'?'})
 
 
-	frags, err := wire.ICBMFragmentList(string(text))
+	frags, err := wire.ICBMFragmentListASCII(string(text))
 	if err != nil {
 	if err != nil {
 		return nil, fmt.Errorf("unable to unmarshal ICBM message: %w", err)
 		return nil, fmt.Errorf("unable to unmarshal ICBM message: %w", err)
 	}
 	}
@@ -686,9 +674,9 @@ func (s *ICBMService) OfflineRetrieve(ctx context.Context, instance *state.Sessi
 			// The sender's capabilities are unknown at retrieval time, so the
 			// The sender's capabilities are unknown at retrieval time, so the
 			// message charset alone decides the conversion. A TLV that fails to
 			// message charset alone decides the conversion. A TLV that fails to
 			// transcode is delivered as stored.
 			// transcode is delivered as stored.
-			if (clientIM.ChannelID == wire.ICBMChannelIM || clientIM.ChannelID == wire.ICBMChannelMIME) &&
-				tlv.Tag == wire.ICBMTLVAOLIMData {
-				if transcoded, err := transcodeMessage(s.legacyCharset, !recipUnicode, recipUnicode, tlv); err != nil {
+			if tlv.Tag == wire.ICBMTLVAOLIMData && instance.UIN() != 0 &&
+				(clientIM.ChannelID == wire.ICBMChannelIM || clientIM.ChannelID == wire.ICBMChannelMIME) {
+				if transcoded, err := transcodeICQMessage(s.legacyCharset, recipUnicode, tlv); err != nil {
 					s.logger.WarnContext(ctx, "unable to transcode offline message", "sender", event.Sender, "err", err)
 					s.logger.WarnContext(ctx, "unable to transcode offline message", "sender", event.Sender, "err", err)
 				} else {
 				} else {
 					tlv = transcoded
 					tlv = transcoded

+ 99 - 11
foodgroup/icbm_test.go

@@ -2183,7 +2183,7 @@ func TestICBMService_ChannelMsgToHost(t *testing.T) {
 			wantErr:      nil,
 			wantErr:      nil,
 		},
 		},
 		{
 		{
-			name:     "unicode sender to legacy recipient, only the message text TLV is transcoded",
+			name:     "unicode sender to legacy ICQ recipient, only the message text TLV is transcoded",
 			instance: newTestInstance("sender-screen-name", sessOptCaps(wire.CapUTF8Messages)),
 			instance: newTestInstance("sender-screen-name", sessOptCaps(wire.CapUTF8Messages)),
 			mockParams: mockParams{
 			mockParams: mockParams{
 				relationshipFetcherParams: relationshipFetcherParams{
 				relationshipFetcherParams: relationshipFetcherParams{
@@ -2199,7 +2199,7 @@ func TestICBMService_ChannelMsgToHost(t *testing.T) {
 					retrieveSessionParams{
 					retrieveSessionParams{
 						{
 						{
 							screenName: state.NewIdentScreenName("recipient-screen-name"),
 							screenName: state.NewIdentScreenName("recipient-screen-name"),
-							result:     newTestInstance("recipient-screen-name", sessOptSignonComplete).Session(),
+							result:     newTestInstance("recipient-screen-name", sessOptSignonComplete, sessOptUIN(100003)).Session(),
 						},
 						},
 					},
 					},
 				},
 				},
@@ -2243,7 +2243,7 @@ func TestICBMService_ChannelMsgToHost(t *testing.T) {
 			expectOutput: nil,
 			expectOutput: nil,
 		},
 		},
 		{
 		{
-			name:     "unicode sender to legacy recipient, message text TLV that fails to transcode is delivered as sent",
+			name:     "unicode sender to legacy AIM recipient, message is delivered as sent",
 			instance: newTestInstance("sender-screen-name", sessOptCaps(wire.CapUTF8Messages)),
 			instance: newTestInstance("sender-screen-name", sessOptCaps(wire.CapUTF8Messages)),
 			mockParams: mockParams{
 			mockParams: mockParams{
 				relationshipFetcherParams: relationshipFetcherParams{
 				relationshipFetcherParams: relationshipFetcherParams{
@@ -2263,6 +2263,66 @@ func TestICBMService_ChannelMsgToHost(t *testing.T) {
 						},
 						},
 					},
 					},
 				},
 				},
+				messageRelayerParams: messageRelayerParams{
+					relayToScreenNameActiveOnlyParams: relayToScreenNameActiveOnlyParams{
+						{
+							screenName: state.NewIdentScreenName("recipient-screen-name"),
+							message: wire.SNACMessage{
+								Frame: wire.SNACFrame{
+									FoodGroup: wire.ICBM,
+									SubGroup:  wire.ICBMChannelMsgToClient,
+									RequestID: wire.ReqIDFromServer,
+								},
+								Body: wire.SNAC_0x04_0x07_ICBMChannelMsgToClient{
+									ChannelID:   wire.ICBMChannelIM,
+									TLVUserInfo: newTestInstance("sender-screen-name", sessOptCaps(wire.CapUTF8Messages)).Session().TLVUserInfo(),
+									TLVRestBlock: wire.TLVRestBlock{
+										TLVList: wire.TLVList{
+											wire.NewTLVBE(wire.ICBMTLVAOLIMData, mustICBMFragmentListUnicode(t, "привет")),
+											wire.NewTLVBE(wire.ICBMTLVAutoResponse, []byte{}),
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			inputSNAC: wire.SNACMessage{
+				Body: wire.SNAC_0x04_0x06_ICBMChannelMsgToHost{
+					ChannelID:  wire.ICBMChannelIM,
+					ScreenName: "recipient-screen-name",
+					TLVRestBlock: wire.TLVRestBlock{
+						TLVList: wire.TLVList{
+							wire.NewTLVBE(wire.ICBMTLVAOLIMData, mustICBMFragmentListUnicode(t, "привет")),
+							wire.NewTLVBE(wire.ICBMTLVAutoResponse, []byte{}),
+						},
+					},
+				},
+			},
+			expectOutput: nil,
+		},
+		{
+			name:     "unicode sender to legacy ICQ recipient, message text TLV that fails to transcode is delivered as sent",
+			instance: newTestInstance("sender-screen-name", sessOptCaps(wire.CapUTF8Messages)),
+			mockParams: mockParams{
+				relationshipFetcherParams: relationshipFetcherParams{
+					relationshipParams: relationshipParams{
+						{
+							me:     state.NewIdentScreenName("sender-screen-name"),
+							them:   state.NewIdentScreenName("recipient-screen-name"),
+							result: state.Relationship{User: state.NewIdentScreenName("recipient-screen-name")},
+						},
+					},
+				},
+				sessionRetrieverParams: sessionRetrieverParams{
+					retrieveSessionParams{
+						{
+							screenName: state.NewIdentScreenName("recipient-screen-name"),
+							result:     newTestInstance("recipient-screen-name", sessOptSignonComplete, sessOptUIN(100003)).Session(),
+						},
+					},
+				},
 				messageRelayerParams: messageRelayerParams{
 				messageRelayerParams: messageRelayerParams{
 					relayToScreenNameActiveOnlyParams: relayToScreenNameActiveOnlyParams{
 					relayToScreenNameActiveOnlyParams: relayToScreenNameActiveOnlyParams{
 						{
 						{
@@ -2394,7 +2454,7 @@ func TestICBMService_ChannelMsgToHost(t *testing.T) {
 
 
 func mustICBMFragmentList(t *testing.T, text string) []wire.ICBMCh1Fragment {
 func mustICBMFragmentList(t *testing.T, text string) []wire.ICBMCh1Fragment {
 	t.Helper()
 	t.Helper()
-	frags, err := wire.ICBMFragmentList(text)
+	frags, err := wire.ICBMFragmentListASCII(text)
 	assert.NoError(t, err)
 	assert.NoError(t, err)
 	return frags
 	return frags
 }
 }
@@ -2467,7 +2527,7 @@ func TestTranscodeMessage(t *testing.T) {
 	// text down to Windows-1251, mirroring wire.ICBMFragmentList.
 	// text down to Windows-1251, mirroring wire.ICBMFragmentList.
 	windows1251TLV := func(t *testing.T, windows1251Bytes []byte) wire.TLV {
 	windows1251TLV := func(t *testing.T, windows1251Bytes []byte) wire.TLV {
 		t.Helper()
 		t.Helper()
-		frags, err := wire.ICBMFragmentList(string(windows1251Bytes))
+		frags, err := wire.ICBMFragmentListASCII(string(windows1251Bytes))
 		assert.NoError(t, err)
 		assert.NoError(t, err)
 		return wire.NewTLVBE(wire.ICBMTLVAOLIMData, frags)
 		return wire.NewTLVBE(wire.ICBMTLVAOLIMData, frags)
 	}
 	}
@@ -2606,7 +2666,7 @@ func TestTranscodeMessage(t *testing.T) {
 			if charset == nil {
 			if charset == nil {
 				charset = charmap.Windows1251
 				charset = charmap.Windows1251
 			}
 			}
-			got, err := transcodeMessage(charset, tt.sender.HasCap(wire.CapUTF8Messages), tt.recip.HasCap(wire.CapUTF8Messages), tt.tlv)
+			got, err := transcodeICQMessage(charset, tt.recip.HasCap(wire.CapUTF8Messages), tt.tlv)
 			if tt.wantErr {
 			if tt.wantErr {
 				assert.Error(t, err)
 				assert.Error(t, err)
 				assert.Contains(t, err.Error(), tt.wantErrText)
 				assert.Contains(t, err.Error(), tt.wantErrText)
@@ -3703,8 +3763,8 @@ func TestICBMService_OfflineRetrieve(t *testing.T) {
 			},
 			},
 		},
 		},
 		{
 		{
-			name:           "unicode message to legacy recipient is transcoded to the legacy charset",
-			senderInstance: newTestInstance("recipient"),
+			name:           "unicode message to legacy ICQ recipient is transcoded to the legacy charset",
+			senderInstance: newTestInstance("recipient", sessOptUIN(100003)),
 			inputSNAC:      wire.SNACMessage{Frame: wire.SNACFrame{RequestID: 42}},
 			inputSNAC:      wire.SNACMessage{Frame: wire.SNACFrame{RequestID: 42}},
 			expectOutput:   successReply,
 			expectOutput:   successReply,
 			mockParams: mockParams{
 			mockParams: mockParams{
@@ -3731,8 +3791,36 @@ func TestICBMService_OfflineRetrieve(t *testing.T) {
 			},
 			},
 		},
 		},
 		{
 		{
-			name:           "legacy charset message to unicode recipient is transcoded to unicode",
-			senderInstance: newTestInstance("recipient", sessOptCaps(wire.CapUTF8Messages)),
+			name:           "unicode message to legacy AIM recipient is delivered as stored",
+			senderInstance: newTestInstance("recipient"),
+			inputSNAC:      wire.SNACMessage{Frame: wire.SNACFrame{RequestID: 42}},
+			expectOutput:   successReply,
+			mockParams: mockParams{
+				offlineMessageManagerParams: offlineMessageManagerParams{
+					retrieveMessagesParams: retrieveMessagesParams{
+						{
+							recipIn: state.NewIdentScreenName("recipient"),
+							messagesOut: storedIM(
+								wire.NewTLVBE(wire.ICBMTLVAOLIMData, mustICBMFragmentListUnicode(t, "привет")),
+								wire.NewTLVBE(wire.ICBMTLVAutoResponse, []byte{}),
+							),
+						},
+					},
+					deleteMessagesParams: deleteMessagesParams{
+						{recipIn: state.NewIdentScreenName("recipient")},
+					},
+				},
+				messageRelayerParams: messageRelayerParams{
+					relayToSelfParams: relayedIM(
+						wire.NewTLVBE(wire.ICBMTLVAOLIMData, mustICBMFragmentListUnicode(t, "привет")),
+						wire.NewTLVBE(wire.ICBMTLVAutoResponse, []byte{}),
+					),
+				},
+			},
+		},
+		{
+			name:           "legacy charset message to unicode ICQ recipient is transcoded to unicode",
+			senderInstance: newTestInstance("recipient", sessOptCaps(wire.CapUTF8Messages), sessOptUIN(100003)),
 			inputSNAC:      wire.SNACMessage{Frame: wire.SNACFrame{RequestID: 42}},
 			inputSNAC:      wire.SNACMessage{Frame: wire.SNACFrame{RequestID: 42}},
 			expectOutput:   successReply,
 			expectOutput:   successReply,
 			mockParams: mockParams{
 			mockParams: mockParams{
@@ -3758,7 +3846,7 @@ func TestICBMService_OfflineRetrieve(t *testing.T) {
 		},
 		},
 		{
 		{
 			name:           "message that fails to transcode is delivered as stored",
 			name:           "message that fails to transcode is delivered as stored",
-			senderInstance: newTestInstance("recipient"),
+			senderInstance: newTestInstance("recipient", sessOptUIN(100003)),
 			inputSNAC:      wire.SNACMessage{Frame: wire.SNACFrame{RequestID: 42}},
 			inputSNAC:      wire.SNACMessage{Frame: wire.SNACFrame{RequestID: 42}},
 			expectOutput:   successReply,
 			expectOutput:   successReply,
 			mockParams: mockParams{
 			mockParams: mockParams{

+ 5 - 5
foodgroup/icq.go

@@ -389,8 +389,6 @@ func (s *ICQService) OfflineMsgReq(ctx context.Context, inFrame wire.SNACFrame,
 		return fmt.Errorf("retrieving messages: %w", err)
 		return fmt.Errorf("retrieving messages: %w", err)
 	}
 	}
 
 
-	recipUnicode := instance.Session().HasCap(wire.CapUTF8Messages)
-
 	for _, msgIn := range messages {
 	for _, msgIn := range messages {
 		if msgIn.Sender.UIN() != 0 {
 		if msgIn.Sender.UIN() != 0 {
 			reply := wire.ICQ_0x0041_DBQueryOfflineMsgReply{
 			reply := wire.ICQ_0x0041_DBQueryOfflineMsgReply{
@@ -415,9 +413,9 @@ func (s *ICQService) OfflineMsgReq(ctx context.Context, inFrame wire.SNACFrame,
 					// the message charset alone decides the conversion. A message
 					// the message charset alone decides the conversion. A message
 					// that fails to transcode is delivered as stored.
 					// that fails to transcode is delivered as stored.
 					tlv := wire.TLV{Tag: wire.ICBMTLVAOLIMData, Value: payload}
 					tlv := wire.TLV{Tag: wire.ICBMTLVAOLIMData, Value: payload}
-					// ICQ doesn't appear to support unicode offline messages, so
+					// ICQ doesn't appear to support Unicode offline messages, so
 					// force a conversion to fallback character set
 					// force a conversion to fallback character set
-					if transcoded, err := transcodeMessage(s.legacyCharset, true, false, tlv); err != nil {
+					if transcoded, err := transcodeICQMessage(s.legacyCharset, false, tlv); err != nil {
 						s.logger.WarnContext(ctx, "unable to transcode offline message", "sender", msgIn.Sender, "err", err)
 						s.logger.WarnContext(ctx, "unable to transcode offline message", "sender", msgIn.Sender, "err", err)
 					} else {
 					} else {
 						tlv = transcoded
 						tlv = transcoded
@@ -490,7 +488,9 @@ func (s *ICQService) OfflineMsgReq(ctx context.Context, inFrame wire.SNACFrame,
 				// to transcode is delivered as stored.
 				// to transcode is delivered as stored.
 				if (clientIM.ChannelID == wire.ICBMChannelIM || clientIM.ChannelID == wire.ICBMChannelMIME) &&
 				if (clientIM.ChannelID == wire.ICBMChannelIM || clientIM.ChannelID == wire.ICBMChannelMIME) &&
 					tlv.Tag == wire.ICBMTLVAOLIMData {
 					tlv.Tag == wire.ICBMTLVAOLIMData {
-					if transcoded, err := transcodeMessage(s.legacyCharset, !recipUnicode, recipUnicode, tlv); err != nil {
+					// ICQ doesn't appear to support Unicode offline messages, so
+					// force a conversion to fallback character set
+					if transcoded, err := transcodeICQMessage(s.legacyCharset, false, tlv); err != nil {
 						s.logger.WarnContext(ctx, "unable to transcode offline message", "sender", msgIn.Sender, "err", err)
 						s.logger.WarnContext(ctx, "unable to transcode offline message", "sender", msgIn.Sender, "err", err)
 					} else {
 					} else {
 						tlv = transcoded
 						tlv = transcoded

+ 3 - 3
foodgroup/icq_test.go

@@ -2337,7 +2337,7 @@ func TestICQService_OfflineMsgReq(t *testing.T) {
 										TLVRestBlock: wire.TLVRestBlock{
 										TLVRestBlock: wire.TLVRestBlock{
 											TLVList: wire.TLVList{
 											TLVList: wire.TLVList{
 												wire.NewTLVBE(wire.ICBMTLVAOLIMData, func() []wire.ICBMCh1Fragment {
 												wire.NewTLVBE(wire.ICBMTLVAOLIMData, func() []wire.ICBMCh1Fragment {
-													frags, err := wire.ICBMFragmentList("hello!")
+													frags, err := wire.ICBMFragmentListASCII("hello!")
 													assert.NoError(t, err)
 													assert.NoError(t, err)
 													return frags
 													return frags
 												}()),
 												}()),
@@ -2563,7 +2563,7 @@ func TestICQService_OfflineMsgReq(t *testing.T) {
 										TLVRestBlock: wire.TLVRestBlock{
 										TLVRestBlock: wire.TLVRestBlock{
 											TLVList: wire.TLVList{
 											TLVList: wire.TLVList{
 												wire.NewTLVBE(wire.ICBMTLVAOLIMData, func() []wire.ICBMCh1Fragment {
 												wire.NewTLVBE(wire.ICBMTLVAOLIMData, func() []wire.ICBMCh1Fragment {
-													frags, err := wire.ICBMFragmentList("hello from AIM!")
+													frags, err := wire.ICBMFragmentListASCII("hello from AIM!")
 													assert.NoError(t, err)
 													assert.NoError(t, err)
 													return frags
 													return frags
 												}()),
 												}()),
@@ -2597,7 +2597,7 @@ func TestICQService_OfflineMsgReq(t *testing.T) {
 										},
 										},
 										TLVRestBlock: wire.TLVRestBlock{},
 										TLVRestBlock: wire.TLVRestBlock{},
 									}
 									}
-									frags, err := wire.ICBMFragmentList("hello from AIM!")
+									frags, err := wire.ICBMFragmentListASCII("hello from AIM!")
 									assert.NoError(t, err)
 									assert.NoError(t, err)
 									msg.Append(wire.NewTLVBE(wire.ICBMTLVAOLIMData, frags))
 									msg.Append(wire.NewTLVBE(wire.ICBMTLVAOLIMData, frags))
 									msg.Append(wire.NewTLVBE(wire.ICBMTLVSendTime, uint32(time.Date(2024, time.August, 2, 12, 5, 0, 0, time.UTC).Unix())))
 									msg.Append(wire.NewTLVBE(wire.ICBMTLVSendTime, uint32(time.Date(2024, time.August, 2, 12, 5, 0, 0, time.UTC).Unix())))

+ 1 - 1
foodgroup/oservice.go

@@ -888,7 +888,7 @@ func (s OServiceService) sendMultipleInstanceNotification(ctx context.Context, i
 }
 }
 
 
 func systemMessage(msg string) (wire.SNACMessage, error) {
 func systemMessage(msg string) (wire.SNACMessage, error) {
-	frags, err := wire.ICBMFragmentList(msg)
+	frags, err := wire.ICBMFragmentListASCII(msg)
 	if err != nil {
 	if err != nil {
 		return wire.SNACMessage{}, fmt.Errorf("creating ICBM fragments: %w", err)
 		return wire.SNACMessage{}, fmt.Errorf("creating ICBM fragments: %w", err)
 	}
 	}

+ 8 - 12
server/http/mgmt_api.go

@@ -633,13 +633,6 @@ func postInstantMessageHandler(w http.ResponseWriter, r *http.Request, icbmSende
 		return
 		return
 	}
 	}
 
 
-	tlv, err := wire.ICBMFragmentListUnicode(input.Text)
-	if err != nil {
-		logger.Error("error sending message POST /instant-message", "err", err.Error())
-		http.Error(w, "internal server error", http.StatusInternalServerError)
-		return
-	}
-
 	sess := state.NewSession()
 	sess := state.NewSession()
 	sess.SetIdentScreenName(state.NewIdentScreenName(input.From))
 	sess.SetIdentScreenName(state.NewIdentScreenName(input.From))
 	sess.SetDisplayScreenName(state.DisplayScreenName(input.From))
 	sess.SetDisplayScreenName(state.DisplayScreenName(input.From))
@@ -654,13 +647,16 @@ func postInstantMessageHandler(w http.ResponseWriter, r *http.Request, icbmSende
 		Cookie:     uint64(time.Now().UnixNano()),
 		Cookie:     uint64(time.Now().UnixNano()),
 		ChannelID:  wire.ICBMChannelIM,
 		ChannelID:  wire.ICBMChannelIM,
 		ScreenName: input.To,
 		ScreenName: input.To,
-		TLVRestBlock: wire.TLVRestBlock{
-			TLVList: wire.TLVList{
-				wire.NewTLVBE(wire.ICBMTLVAOLIMData, tlv),
-			},
-		},
 	}
 	}
 
 
+	tlv, err := wire.ICBMFragmentList(input.Text)
+	if err != nil {
+		logger.Error("error sending message POST /instant-message", "err", err.Error())
+		http.Error(w, "internal server error", http.StatusInternalServerError)
+		return
+	}
+	body.Append(wire.NewTLVBE(wire.ICBMTLVAOLIMData, tlv))
+
 	reply, err := icbmSender(r.Context(), instance, frame, body)
 	reply, err := icbmSender(r.Context(), instance, frame, body)
 	if err != nil {
 	if err != nil {
 		logger.Error("error sending message POST /instant-message", "err", err.Error())
 		logger.Error("error sending message POST /instant-message", "err", err.Error())

+ 10 - 5
server/http/mgmt_api_test.go

@@ -1890,6 +1890,15 @@ func TestInstantMessageHandler_POST(t *testing.T) {
 
 
 				assert.Equal(t, [][16]byte{wire.CapUTF8Messages}, instance.Caps())
 				assert.Equal(t, [][16]byte{wire.CapUTF8Messages}, instance.Caps())
 
 
+				wantText := tc.wantText
+				if wantText == "" {
+					wantText = "hello world!"
+				}
+				wantCharset := wire.ICBMMessageEncodingUnicode
+				if wire.AllASCII(wantText) {
+					wantCharset = wire.ICBMMessageEncodingASCII
+				}
+
 				b, ok := inBody.Bytes(wire.ICBMTLVAOLIMData)
 				b, ok := inBody.Bytes(wire.ICBMTLVAOLIMData)
 				assert.True(t, ok)
 				assert.True(t, ok)
 
 
@@ -1899,14 +1908,10 @@ func TestInstantMessageHandler_POST(t *testing.T) {
 					if frag.ID == 1 {
 					if frag.ID == 1 {
 						msg := wire.ICBMCh1Message{}
 						msg := wire.ICBMCh1Message{}
 						assert.NoError(t, wire.UnmarshalBE(&msg, bytes.NewBuffer(frag.Payload)))
 						assert.NoError(t, wire.UnmarshalBE(&msg, bytes.NewBuffer(frag.Payload)))
-						assert.Equal(t, wire.ICBMMessageEncodingUnicode, msg.Charset)
+						assert.Equal(t, wantCharset, msg.Charset)
 					}
 					}
 				}
 				}
 
 
-				wantText := tc.wantText
-				if wantText == "" {
-					wantText = "hello world!"
-				}
 				txt, err := wire.UnmarshalICBMMessageText(b)
 				txt, err := wire.UnmarshalICBMMessageText(b)
 				assert.NoError(t, err)
 				assert.NoError(t, err)
 				assert.Equal(t, wantText, txt)
 				assert.Equal(t, wantText, txt)

+ 1 - 1
server/icq_legacy/service_test.go

@@ -816,7 +816,7 @@ func TestICQLegacyService_GetOfflineMessages(t *testing.T) {
 										ChannelID: wire.ICBMChannelIM,
 										ChannelID: wire.ICBMChannelIM,
 										TLVRestBlock: wire.TLVRestBlock{
 										TLVRestBlock: wire.TLVRestBlock{
 											TLVList: func() wire.TLVList {
 											TLVList: func() wire.TLVList {
-												frags, _ := wire.ICBMFragmentList("hello offline")
+												frags, _ := wire.ICBMFragmentListASCII("hello offline")
 												return wire.TLVList{
 												return wire.TLVList{
 													wire.NewTLVBE(wire.ICBMTLVAOLIMData, frags),
 													wire.NewTLVBE(wire.ICBMTLVAOLIMData, frags),
 												}
 												}

+ 1 - 1
server/toc/cmd_client.go

@@ -1931,7 +1931,7 @@ func (s OSCARProxy) SendIM(ctx context.Context, sender *state.SessionInstance, a
 }
 }
 
 
 func (s OSCARProxy) sendIm(ctx context.Context, sender *state.SessionInstance, msg string, recip string, autoReply []string) []string {
 func (s OSCARProxy) sendIm(ctx context.Context, sender *state.SessionInstance, msg string, recip string, autoReply []string) []string {
-	frags, err := wire.ICBMFragmentList(unescape(msg))
+	frags, err := wire.ICBMFragmentListASCII(unescape(msg))
 	if err != nil {
 	if err != nil {
 		return s.runtimeErr(ctx, fmt.Errorf("wire.ICBMFragmentList: %w", err))
 		return s.runtimeErr(ctx, fmt.Errorf("wire.ICBMFragmentList: %w", err))
 	}
 	}

+ 1 - 3
server/webapi/im_handler.go

@@ -73,13 +73,11 @@ func (h *MessagingHandler) SendIM(w http.ResponseWriter, r *http.Request, sess *
 		TLVRestBlock: wire.TLVRestBlock{},
 		TLVRestBlock: wire.TLVRestBlock{},
 	}
 	}
 
 
-	// Add message data
-	frags, err := wire.ICBMFragmentListUnicode(message)
+	frags, err := wire.ICBMFragmentList(message)
 	if err != nil {
 	if err != nil {
 		SendError(w, r, http.StatusInternalServerError, "failed to send message")
 		SendError(w, r, http.StatusInternalServerError, "failed to send message")
 		return
 		return
 	}
 	}
-
 	clientIM.Append(wire.NewTLVBE(wire.ICBMTLVAOLIMData, frags))
 	clientIM.Append(wire.NewTLVBE(wire.ICBMTLVAOLIMData, frags))
 
 
 	// Add auto-response flag if applicable
 	// Add auto-response flag if applicable

+ 5 - 5
server/webapi/session_test.go

@@ -384,7 +384,7 @@ func TestSession_RepeatsBuddyAliasOnOSCAREvents(t *testing.T) {
 
 
 	t.Run("incoming IM", func(t *testing.T) {
 	t.Run("incoming IM", func(t *testing.T) {
 		sess := newSession()
 		sess := newSession()
-		frags, err := wire.ICBMFragmentList("hello")
+		frags, err := wire.ICBMFragmentListASCII("hello")
 		require.NoError(t, err)
 		require.NoError(t, err)
 		body := wire.SNAC_0x04_0x07_ICBMChannelMsgToClient{
 		body := wire.SNAC_0x04_0x07_ICBMChannelMsgToClient{
 			ChannelID:   wire.ICBMChannelIM,
 			ChannelID:   wire.ICBMChannelIM,
@@ -623,7 +623,7 @@ func TestSession_HandleIncomingIM_NormalizesAimID(t *testing.T) {
 		FeedbagLoader: emptyFeedbagLoader,
 		FeedbagLoader: emptyFeedbagLoader,
 	}
 	}
 
 
-	frags, err := wire.ICBMFragmentList("hello")
+	frags, err := wire.ICBMFragmentListASCII("hello")
 	assert.NoError(t, err)
 	assert.NoError(t, err)
 
 
 	body := wire.SNAC_0x04_0x07_ICBMChannelMsgToClient{
 	body := wire.SNAC_0x04_0x07_ICBMChannelMsgToClient{
@@ -1090,7 +1090,7 @@ func TestSession_OfflineIM(t *testing.T) {
 
 
 	storedMsg := func(t *testing.T, withSendTime bool) wire.SNACMessage {
 	storedMsg := func(t *testing.T, withSendTime bool) wire.SNACMessage {
 		t.Helper()
 		t.Helper()
-		frags, err := wire.ICBMFragmentList("sent while you were out")
+		frags, err := wire.ICBMFragmentListASCII("sent while you were out")
 		require.NoError(t, err)
 		require.NoError(t, err)
 		body := wire.SNAC_0x04_0x07_ICBMChannelMsgToClient{
 		body := wire.SNAC_0x04_0x07_ICBMChannelMsgToClient{
 			ChannelID:   wire.ICBMChannelIM,
 			ChannelID:   wire.ICBMChannelIM,
@@ -1638,7 +1638,7 @@ func TestSession_IncomingIMReportsSenderPresenceFromTheView(t *testing.T) {
 
 
 	incomingIM := func(t *testing.T, sess *Session, from string) IMEvent {
 	incomingIM := func(t *testing.T, sess *Session, from string) IMEvent {
 		t.Helper()
 		t.Helper()
-		frags, err := wire.ICBMFragmentList("hello")
+		frags, err := wire.ICBMFragmentListASCII("hello")
 		require.NoError(t, err)
 		require.NoError(t, err)
 		body := wire.SNAC_0x04_0x07_ICBMChannelMsgToClient{
 		body := wire.SNAC_0x04_0x07_ICBMChannelMsgToClient{
 			ChannelID:   wire.ICBMChannelIM,
 			ChannelID:   wire.ICBMChannelIM,
@@ -1984,7 +1984,7 @@ func TestSession_ForgetBuddyPresence(t *testing.T) {
 		buddyArrives(sess, onlineBuddy("Mike Kelly"))
 		buddyArrives(sess, onlineBuddy("Mike Kelly"))
 		sess.forgetBuddyPresence(state.NewIdentScreenName("mikekelly"))
 		sess.forgetBuddyPresence(state.NewIdentScreenName("mikekelly"))
 
 
-		frags, err := wire.ICBMFragmentList("hello")
+		frags, err := wire.ICBMFragmentListASCII("hello")
 		require.NoError(t, err)
 		require.NoError(t, err)
 		body := wire.SNAC_0x04_0x07_ICBMChannelMsgToClient{
 		body := wire.SNAC_0x04_0x07_ICBMChannelMsgToClient{
 			ChannelID:   wire.ICBMChannelIM,
 			ChannelID:   wire.ICBMChannelIM,

+ 19 - 3
wire/snacs.go

@@ -910,9 +910,25 @@ type SNAC_0x04_0x07_ICBMChannelMsgToClient struct {
 	TLVRestBlock
 	TLVRestBlock
 }
 }
 
 
-// ICBMFragmentList creates an ICBM fragment list for an instant message
-// payload.
+func AllASCII[T string | []byte](msg T) bool {
+	for i := 0; i < len(msg); i++ {
+		if msg[i] > unicode.MaxASCII {
+			return false
+		}
+	}
+	return true
+}
+
 func ICBMFragmentList(text string) ([]ICBMCh1Fragment, error) {
 func ICBMFragmentList(text string) ([]ICBMCh1Fragment, error) {
+	if AllASCII(text) {
+		return ICBMFragmentListASCII(text)
+	}
+	return ICBMFragmentListUnicode(text)
+}
+
+// ICBMFragmentListASCII creates an ICBM fragment list for an instant message
+// payload.
+func ICBMFragmentListASCII(text string) ([]ICBMCh1Fragment, error) {
 	msg := ICBMCh1Message{
 	msg := ICBMCh1Message{
 		Charset:  ICBMMessageEncodingASCII,
 		Charset:  ICBMMessageEncodingASCII,
 		Language: 0, // not clear what this means, but it works
 		Language: 0, // not clear what this means, but it works
@@ -937,7 +953,7 @@ func ICBMFragmentList(text string) ([]ICBMCh1Fragment, error) {
 	}, nil
 	}, nil
 }
 }
 
 
-// ICBMFragmentList creates an ICBM fragment list for an instant message
+// ICBMFragmentListUnicode creates an ICBM fragment list for an instant message
 // payload.
 // payload.
 func ICBMFragmentListUnicode(text string) ([]ICBMCh1Fragment, error) {
 func ICBMFragmentListUnicode(text string) ([]ICBMCh1Fragment, error) {
 
 

+ 41 - 0
wire/snacs_test.go

@@ -481,3 +481,44 @@ func TestFeedbagItem_RemoveOrderMembers(t *testing.T) {
 		assert.Equal(t, []uint16{5, 10}, order)
 		assert.Equal(t, []uint16{5, 10}, order)
 	})
 	})
 }
 }
+
+func TestAllASCII(t *testing.T) {
+	tests := []struct {
+		name string
+		msg  string
+		want bool
+	}{
+		{name: "empty", msg: "", want: true},
+		{name: "ASCII", msg: "hello world!", want: true},
+		{name: "DEL is ASCII", msg: "\x7f", want: true},
+		{name: "Latin-1 byte", msg: "\xe9", want: false},
+		{name: "multibyte UTF-8", msg: "héllo", want: false},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert.Equal(t, tt.want, AllASCII(tt.msg))
+			assert.Equal(t, tt.want, AllASCII([]byte(tt.msg)))
+		})
+	}
+}
+
+func TestICBMFragmentList(t *testing.T) {
+	tests := []struct {
+		name string
+		text string
+		want func(string) ([]ICBMCh1Fragment, error)
+	}{
+		{name: "ASCII text is encoded as ASCII", text: "hello world!", want: ICBMFragmentListASCII},
+		{name: "empty text is encoded as ASCII", text: "", want: ICBMFragmentListASCII},
+		{name: "non-ASCII text is encoded as UCS-2", text: "héllo 日本", want: ICBMFragmentListUnicode},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			want, err := tt.want(tt.text)
+			assert.NoError(t, err)
+			got, err := ICBMFragmentList(tt.text)
+			assert.NoError(t, err)
+			assert.Equal(t, want, got)
+		})
+	}
+}