Repository: slack-go/slack (go)

Issue:
Unable to disable emojis in plain text blocks
### What happened

Creating a `TextBlockObject` with the `Emoji` field set to `false` **does not** disable rendering of emojis in that text block.

The reason this happens is because of the `omitempty` JSON-tag:

```go
type TextBlockObject struct {
	Type     string `json:"type"`
	Text     string `json:"text"`
	Emoji    bool   `json:"emoji,omitempty"` // <-- this one
	Verbatim bool   `json:"verbatim,omitempty"`
}
```

This causes the field to be removed from the payload when encoded to JSON (if the field is `false`) as seen in this snippet: https://go.dev/play/p/63tqgJJ6Nnt.

The required JSON payload for creating a plain text object without support for emojis is:

```json
{
	"type": "section",
	"text": {
		"type": "plain_text",
		"text": ":+1:",
		"emoji": false
	}
}
```

This is likely an issue for some other fields with the `omitempty` tag as well, where you explicitly need to set a value to `false` in the JSON payload for the Slack API.

### Expected behavior

Creating a `TextBlockObject` with the `Emoji` field set to `false` **should** disable rendering of emojis in that text block.

### Steps to reproduce

```go
emoji, verbatim := false, false
slack.New("some token").PostMessage(
	"channel ID",
	slack.MsgOptionBlocks(
		slack.NewSectionBlock(
			slack.NewTextBlockObject("plain_text", ":+1:", emoji, verbatim), 
			nil, 
			nil,
		),
	),
)
```

### Versions
- Go: `go version go1.23.1 linux/amd64`
- slack-go/slack: `v0.14.0`


Pull request:
fix: Allow `emoji` value in plain text blocks to be null
Fixes #1332.

This PR allows setting null values for the `emoji` field of plain text blocks, which was previously incorrectly marshalling to JSON due to a "false" value being omitted. I opted not to change the signature of `NewTextBlockObject`, since the null value is only expected when unmarshalling, and the user should know which value they need to use when creating the block.

Patch:
diff --git a/block_object.go b/block_object.go
index a3e78d4..899ac0d 100644
--- a/block_object.go
+++ b/block_object.go
@@ -122,7 +122,7 @@ func unmarshalBlockObject(r json.RawMessage, object blockObject) (blockObject, e
 type TextBlockObject struct {
 	Type     string `json:"type"`
 	Text     string `json:"text"`
-	Emoji    bool   `json:"emoji,omitempty"`
+	Emoji    *bool  `json:"emoji"`
 	Verbatim bool   `json:"verbatim,omitempty"`
 }
 
@@ -143,7 +143,7 @@ func (s TextBlockObject) Validate() error {
 	}
 
 	// https://github.com/slack-go/slack/issues/881
-	if s.Type == "mrkdwn" && s.Emoji {
+	if s.Type == "mrkdwn" && s.Emoji != nil && *s.Emoji {
 		return errors.New("emoji cannot be true in mrkdown")
 	}
 
@@ -161,11 +161,11 @@ func (s TextBlockObject) Validate() error {
 }
 
 // NewTextBlockObject returns an instance of a new Text Block Object
-func NewTextBlockObject(elementType, text string, emoji, verbatim bool) *TextBlockObject {
+func NewTextBlockObject(elementType, text string, emoji bool, verbatim bool) *TextBlockObject {
 	return &TextBlockObject{
 		Type:     elementType,
 		Text:     text,
-		Emoji:    emoji,
+		Emoji:    &emoji,
 		Verbatim: verbatim,
 	}
 }
diff --git a/files.go b/files.go
index 0d6626b..866b490 100644
--- a/files.go
+++ b/files.go
@@ -94,6 +94,7 @@ type File struct {
 	To      []EmailFileUserInfo `json:"to"`
 	From    []EmailFileUserInfo `json:"from"`
 	Cc      []EmailFileUserInfo `json:"cc"`
+	Headers EmailHeaders        `json:"headers"`
 }
 
 type EmailFileUserInfo struct {
@@ -102,6 +103,13 @@ type EmailFileUserInfo struct {
 	Original string `json:"original"`
 }
 
+type EmailHeaders struct {
+	Date      string `json:"date"`
+	InReplyTo string `json:"in_reply_to"`
+	ReplyTo   string `json:"reply_to"`
+	MessageID string `json:"message_id"`
+}
+
 type Share struct {
 	Public  map[string][]ShareFileInfo `json:"public"`
 	Private map[string][]ShareFileInfo `json:"private"`


Test patch:
diff --git a/attachments_test.go b/attachments_test.go
index 4b951f1..cbb139c 100644
--- a/attachments_test.go
+++ b/attachments_test.go
@@ -19,6 +19,7 @@ func TestAttachment_UnmarshalMarshalJSON_WithBlocks(t *testing.T) {
         "text": {
           "type": "mrkdwn",
           "text": "Pick something:",
+          "emoji": null,
           "verbatim": true
         },
         "accessory": {
diff --git a/block_object_test.go b/block_object_test.go
index b8f55a4..7311034 100644
--- a/block_object_test.go
+++ b/block_object_test.go
@@ -1,10 +1,12 @@
 package slack
 
 import (
+	"encoding/json"
 	"errors"
 	"strings"
 	"testing"
 
+	"github.com/go-test/deep"
 	"github.com/stretchr/testify/assert"
 )
 
@@ -21,7 +23,7 @@ func TestNewTextBlockObject(t *testing.T) {
 
 	assert.Equal(t, textObject.Type, "plain_text")
 	assert.Equal(t, textObject.Text, "test")
-	assert.True(t, textObject.Emoji, "Emoji property should be true")
+	assert.True(t, *textObject.Emoji, "Emoji property should be true")
 	assert.False(t, textObject.Verbatim, "Verbatim should be false")
 }
 
@@ -75,6 +77,11 @@ func TestNewOptionGroupBlockElement(t *testing.T) {
 }
 
 func TestValidateTextBlockObject(t *testing.T) {
+	emojiTrue := new(bool)
+	emojiFalse := new(bool)
+	*emojiTrue = true
+	*emojiFalse = false
+
 	tests := []struct {
 		input    TextBlockObject
 		expected error
@@ -83,7 +90,34 @@ func TestValidateTextBlockObject(t *testing.T) {
 			input: TextBlockObject{
 				Type:     "plain_text",
 				Text:     "testText",
-				Emoji:    false,
+				Emoji:    emojiFalse,
+				Verbatim: false,
+			},
+			expected: nil,
+		},
+		{
+			input: TextBlockObject{
+				Type:     "plain_text",
+				Text:     "testText",
+				Emoji:    emojiTrue,
+				Verbatim: false,
+			},
+			expected: nil,
+		},
+		{
+			input: TextBlockObject{
+				Type:     "plain_text",
+				Text:     "testText",
+				Emoji:    nil,
+				Verbatim: false,
+			},
+			expected: nil,
+		},
+		{
+			input: TextBlockObject{
+				Type:     "mrkdwn",
+				Text:     "testText",
+				Emoji:    emojiFalse,
 				Verbatim: false,
 			},
 			expected: nil,
@@ -92,7 +126,7 @@ func TestValidateTextBlockObject(t *testing.T) {
 			input: TextBlockObject{
 				Type:     "mrkdwn",
 				Text:     "testText",
-				Emoji:    false,
+				Emoji:    nil,
 				Verbatim: false,
 			},
 			expected: nil,
@@ -101,7 +135,7 @@ func TestValidateTextBlockObject(t *testing.T) {
 			input: TextBlockObject{
 				Type:     "invalid",
 				Text:     "testText",
-				Emoji:    false,
+				Emoji:    emojiFalse,
 				Verbatim: false,
 			},
 			expected: errors.New("type must be either of plain_text or mrkdwn"),
@@ -110,7 +144,7 @@ func TestValidateTextBlockObject(t *testing.T) {
 			input: TextBlockObject{
 				Type:     "mrkdwn",
 				Text:     "testText",
-				Emoji:    true,
+				Emoji:    emojiTrue,
 				Verbatim: false,
 			},
 			expected: errors.New("emoji cannot be true in mrkdown"),
@@ -119,7 +153,7 @@ func TestValidateTextBlockObject(t *testing.T) {
 			input: TextBlockObject{
 				Type:     "mrkdwn",
 				Text:     "",
-				Emoji:    false,
+				Emoji:    emojiFalse,
 				Verbatim: false,
 			},
 			expected: errors.New("text must have a minimum length of 1"),
@@ -128,7 +162,7 @@ func TestValidateTextBlockObject(t *testing.T) {
 			input: TextBlockObject{
 				Type:     "mrkdwn",
 				Text:     strings.Repeat("a", 3001),
-				Emoji:    false,
+				Emoji:    emojiFalse,
 				Verbatim: false,
 			},
 			expected: errors.New("text cannot be longer than 3000 characters"),
@@ -140,3 +174,83 @@ func TestValidateTextBlockObject(t *testing.T) {
 		assert.Equal(t, err, test.expected)
 	}
 }
+
+func TestTextBlockObject_UnmarshalJSON(t *testing.T) {
+	emojiTrue := new(bool)
+	emojiFalse := new(bool)
+	*emojiTrue = true
+	*emojiFalse = false
+
+	cases := []struct {
+		raw      []byte
+		expected TextBlockObject
+		err      error
+	}{
+		{
+			[]byte(`{"type":"plain_text","text":"testText"}`),
+			TextBlockObject{
+				Type:     "plain_text",
+				Text:     "testText",
+				Emoji:    nil,
+				Verbatim: false,
+			},
+			nil,
+		},
+		{
+			[]byte(`{"type":"plain_text","text":":+1:","emoji":true}`),
+			TextBlockObject{
+				Type:     "plain_text",
+				Text:     ":+1:",
+				Emoji:    emojiTrue,
+				Verbatim: false,
+			},
+			nil,
+		},
+		{
+			[]byte(`{"type":"plain_text","text":"No emojis allowed :(","emoji":false}`),
+			TextBlockObject{
+				Type:     "plain_text",
+				Text:     "No emojis allowed :(",
+				Emoji:    emojiFalse,
+				Verbatim: false,
+			},
+			nil,
+		},
+		{
+			[]byte(`{"type":"mrkdwn","text":"testText"}`),
+			TextBlockObject{
+				Type:     "mrkdwn",
+				Text:     "testText",
+				Emoji:    nil,
+				Verbatim: false,
+			},
+			nil,
+		},
+		{
+			[]byte(`{"type":"mrkdwn","text":"No emojis allowed :(","emoji":false}`),
+			TextBlockObject{
+				Type:     "mrkdwn",
+				Text:     "No emojis allowed :(",
+				Emoji:    emojiFalse,
+				Verbatim: false,
+			},
+			nil,
+		},
+	}
+	for _, tc := range cases {
+		var actual TextBlockObject
+		err := json.Unmarshal(tc.raw, &actual)
+		if err != nil {
+			if tc.err == nil {
+				t.Errorf("unexpected error: %s", err)
+			}
+			t.Errorf("expected error is %v, but got %v", tc.err, err)
+		}
+		if tc.err != nil {
+			t.Errorf("expected to raise an error %v", tc.err)
+		}
+		if diff := deep.Equal(actual, tc.expected); diff != nil {
+			t.Errorf("actual value does not match expected one\n%s", diff)
+		}
+	}
+}
diff --git a/block_section_test.go b/block_section_test.go
index 8f8374e..c6bd209 100644
--- a/block_section_test.go
+++ b/block_section_test.go
@@ -30,7 +30,7 @@ func TestNewBlockSectionContainsAddedTextBlockAndAccessory(t *testing.T) {
 	textBlockInSection := sectionBlock.Text
 	assert.Equal(t, textBlockInSection.Text, textBlockObject.Text)
 	assert.Equal(t, textBlockInSection.Type, textBlockObject.Type)
-	assert.True(t, textBlockInSection.Emoji)
+	assert.True(t, *textBlockInSection.Emoji)
 	assert.False(t, textBlockInSection.Verbatim)
 	assert.Equal(t, sectionBlock.Accessory.ImageElement, conflictImage)
 }
diff --git a/chat_test.go b/chat_test.go
index 917ff96..ab326f2 100644
--- a/chat_test.go
+++ b/chat_test.go
@@ -81,7 +81,7 @@ func TestPostMessage(t *testing.T) {
 	}
 
 	blocks := []Block{NewContextBlock("context", NewTextBlockObject(PlainTextType, "hello", false, false))}
-	blockStr := `[{"type":"context","block_id":"context","elements":[{"type":"plain_text","text":"hello"}]}]`
+	blockStr := `[{"type":"context","block_id":"context","elements":[{"type":"plain_text","text":"hello","emoji":false}]}]`
 
 	tests := map[string]messageTest{
 		"OnlyBasicProperties": {
diff --git a/interactions_test.go b/interactions_test.go
index 989690e..faf5333 100644
--- a/interactions_test.go
+++ b/interactions_test.go
@@ -51,13 +51,15 @@ const (
 			"type": "modal",
 			"title": {
 				"type": "plain_text",
-				"text": "launch project"
+				"text": "launch project",
+				"emoji": false
 			},
 			"blocks": [{
 				"type": "section",
 				"text": {
 				  "text": "*Sally* has requested you set the deadline for the Nano launch project",
-				  "type": "mrkdwn"
+				  "type": "mrkdwn",
+				  "emoji": false
 				},
 				"accessory": {
 				  "type": "datepicker",
@@ -65,7 +67,8 @@ const (
 				  "initial_date": "1990-04-28",
 				  "placeholder": {
 					"type": "plain_text",
-					"text": "Select a date"
+					"text": "Select a date",
+				  	"emoji": false
 				  }
 				}
 			}],
@@ -92,7 +95,8 @@ const (
 			"type": "modal",
 			"title": {
 				"type": "plain_text",
-				"text": "meal choice"
+				"text": "meal choice",
+				"emoji": false
 			},
 			"blocks": [
 				{
@@ -100,7 +104,8 @@ const (
 					"block_id": "multi-line",
 					"label": {
 						"type": "plain_text",
-						"text": "dietary restrictions"
+						"text": "dietary restrictions",
+				  		"emoji": false
 					},
 					"element": {
 						"type": "plain_text_input",
@@ -113,7 +118,8 @@ const (
 					"block_id": "target_channel",
 					"label": {
 						"type": "plain_text",
-						"text": "Select a channel to post the result on"
+						"text": "Select a channel to post the result on",
+						"emoji": false
 					},
 					"element": {
 						"type": "conversations_select",
diff --git a/views_test.go b/views_test.go
index 5908737..c86c0a0 100644
--- a/views_test.go
+++ b/views_test.go
@@ -767,7 +767,8 @@ func TestSlack_UpdateViewSubmissionResponse(t *testing.T) {
 			"type": "modal",
 			"title": {
 				"type": "plain_text",
-				"text": "Test update view submission response"
+				"text": "Test update view submission response",
+				"emoji": false
 			},
 			"blocks": [
 				{
@@ -803,7 +804,8 @@ func TestSlack_PushViewSubmissionResponse(t *testing.T) {
 			"type": "modal",
 			"title": {
 				"type": "plain_text",
-				"text": "Test update view submission response"
+				"text": "Test update view submission response",
+				"emoji": false
 			},
 			"blocks": [
 				{
@@ -812,7 +814,8 @@ func TestSlack_PushViewSubmissionResponse(t *testing.T) {
 					"elements": [
 						{
 							"type": "plain_text",
-							"text": "Context text"
+							"text": "Context text",
+							"emoji": false
 						},
 						{
 							"type": "image",
diff --git a/webhooks_test.go b/webhooks_test.go
index 90b7e28..4c76a1d 100644
--- a/webhooks_test.go
+++ b/webhooks_test.go
@@ -77,13 +77,13 @@ func TestWebhookMessage_WithBlocks(t *testing.T) {
 	assert.Equal(t, 1, len(msgSingleBlock.Blocks.BlockSet))
 
 	msgJsonSingleBlock, _ := json.Marshal(msgSingleBlock)
-	assert.Equal(t, `{"blocks":[{"type":"section","text":{"type":"plain_text","text":"text"}}],"replace_original":false,"delete_original":false}`, string(msgJsonSingleBlock))
+	assert.Equal(t, `{"blocks":[{"type":"section","text":{"type":"plain_text","text":"text","emoji":false}}],"replace_original":false,"delete_original":false}`, string(msgJsonSingleBlock))
 
 	msgTwoBlocks := WebhookMessage{Blocks: twoBlocks}
 	assert.Equal(t, 2, len(msgTwoBlocks.Blocks.BlockSet))
 
 	msgJsonTwoBlocks, _ := json.Marshal(msgTwoBlocks)
-	assert.Equal(t, `{"blocks":[{"type":"section","text":{"type":"plain_text","text":"text"}},{"type":"section","text":{"type":"plain_text","text":"text"}}],"replace_original":false,"delete_original":false}`, string(msgJsonTwoBlocks))
+	assert.Equal(t, `{"blocks":[{"type":"section","text":{"type":"plain_text","text":"text","emoji":false}},{"type":"section","text":{"type":"plain_text","text":"text","emoji":false}}],"replace_original":false,"delete_original":false}`, string(msgJsonTwoBlocks))
 
 	msgNoBlocks := WebhookMessage{Text: "foo"}
 	msgJsonNoBlocks, _ := json.Marshal(msgNoBlocks)